├── .hgtags ├── AddinMaker.wxg ├── LICENSE ├── README.md ├── addin.py ├── addin_assistant.pyw ├── addin_ui.py ├── i18n.py ├── images ├── AddInDesktop.ico ├── AddInDesktop48.png └── AddInDesktop64.png ├── packaging ├── README.txt └── makeaddin.py ├── resources └── resource_strings.json └── setup.py /.hgtags: -------------------------------------------------------------------------------- 1 | d2c510239e37de29c20718cbbbfc53e94de59e0c 10.2-wizard 2 | -------------------------------------------------------------------------------- /AddinMaker.wxg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | _msize = wx.Size(700, 550) 7 | 8 | ArcGIS Python Add-In Wizard 9 | 1 10 | 11 | _msize 12 | 13 | 14 | wxHORIZONTAL 15 | 16 | wxALL|wxEXPAND 17 | 1 18 | 19 | 20 | 21 | 22 | wxVERTICAL 23 | 24 | wxEXPAND|wxALIGN_RIGHT 25 | 0 26 | 27 | 28 | 29 | wxSYS_COLOUR_3DHIGHLIGHT 30 | 31 | wxHORIZONTAL 32 | 33 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_HORIZONTAL 34 | 8 35 | 36 | 37 | 38 | 1 39 | 40 | 41 | 16 42 | default 43 | 44 | normal 45 | 0 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | wxEXPAND 55 | 0 56 | 57 | 58 | 59 | 1 60 | 61 | 62 | 63 | wxEXPAND 64 | 0 65 | 66 | 67 | 68 | 69 | wxHORIZONTAL 70 | 71 | wxEXPAND 72 | 4 73 | 74 | 75 | wxHORIZONTAL 76 | 77 | wxEXPAND 78 | 0 79 | 80 | 81 | 82 | 83 | wxHORIZONTAL 84 | 85 | wxTOP 86 | 8 87 | 88 | 89 | 1 90 | images\AddInDesktop64.png 91 | 92 | 93 | 94 | 95 | 96 | 97 | wxEXPAND 98 | 4 99 | 100 | 101 | 102 | 103 | Project Settings 104 | Add-In Contents 105 | 106 | 107 | ChangeTab 108 | 109 | 110 | 111 | 112 | wxVERTICAL 113 | 114 | wxALL|wxEXPAND 115 | 4 116 | 117 | 118 | wxHORIZONTAL 119 | 120 | wxALIGN_CENTER_VERTICAL 121 | 8 122 | 123 | 124 | 125 | 1 126 | 127 | 128 | 129 | 130 | wxEXPAND 131 | 8 132 | 133 | 134 | 135 | 136 | SelectFolder 137 | 138 | 139 | 140 | 141 | 142 | 143 | wxALL|wxEXPAND 144 | 4 145 | 146 | 147 | wxHORIZONTAL 148 | 149 | wxALIGN_CENTER_VERTICAL 150 | 3 151 | 152 | 153 | 154 | 1 155 | 156 | 157 | 158 | 159 | 3 160 | 161 | 162 | 163 | 0 164 | 165 | ArcMap 166 | ArcGlobe 167 | ArcScene 168 | ArcCatalog 169 | 170 | 171 | ComboBox 172 | 173 | 174 | 175 | 176 | 177 | 178 | wxALL|wxEXPAND 179 | 8 180 | 181 | 182 | 183 | 1 184 | 185 | 186 | 187 | wxEXPAND 188 | 0 189 | 190 | 191 | wxVERTICAL 192 | 193 | 194 | wxEXPAND 195 | 0 196 | 197 | 198 | wxVERTICAL 199 | 200 | wxEXPAND 201 | 0 202 | 203 | 204 | wxHORIZONTAL 205 | 206 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL 207 | 2 208 | 209 | 210 | 211 | 1 212 | 213 | 72, 16 214 | 215 | 216 | 217 | wxEXPAND 218 | 0 219 | 220 | 221 | 222 | ProjectNameText 223 | ProjectNameText 224 | 225 | 226 | 227 | 228 | 229 | 230 | wxEXPAND 231 | 0 232 | 233 | 234 | wxHORIZONTAL 235 | 236 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL 237 | 2 238 | 239 | 240 | 241 | 1 242 | 243 | 72, 16 244 | 245 | 246 | 247 | wxEXPAND 248 | 0 249 | 250 | 251 | 252 | ProjectVersionText 253 | ProjectVersionText 254 | 255 | 256 | 257 | 258 | 259 | 260 | wxEXPAND 261 | 0 262 | 263 | 264 | wxHORIZONTAL 265 | 266 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL 267 | 2 268 | 269 | 270 | 271 | 1 272 | 273 | 72, 16 274 | 275 | 276 | 277 | wxEXPAND 278 | 0 279 | 280 | 281 | 282 | ProjectCompanyText 283 | ProjectCompanyText 284 | 285 | 286 | 287 | 288 | 289 | 290 | wxEXPAND 291 | 0 292 | 293 | 294 | wxHORIZONTAL 295 | 296 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL 297 | 2 298 | 299 | 300 | 301 | 1 302 | 303 | 72, 16 304 | 305 | 306 | 307 | wxEXPAND 308 | 0 309 | 310 | 311 | 312 | ProjectDescriptionText 313 | ProjectDescriptionText 314 | 315 | 316 | 317 | 318 | 319 | 320 | wxEXPAND 321 | 0 322 | 323 | 324 | wxHORIZONTAL 325 | 326 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL 327 | 2 328 | 329 | 330 | 331 | 1 332 | 333 | 72, 16 334 | 335 | 336 | 337 | wxEXPAND 338 | 0 339 | 340 | 341 | 342 | ProjectAuthorText 343 | ProjectAuthorText 344 | 345 | 346 | 347 | 348 | 349 | 350 | wxALL|wxEXPAND 351 | 2 352 | 353 | 354 | 355 | 1 356 | 357 | 358 | 359 | wxEXPAND 360 | 3 361 | 362 | 363 | wxHORIZONTAL 364 | 365 | wxALL|wxALIGN_RIGHT|wxALIGN_CENTER_VERTICAL 366 | 2 367 | 368 | 369 | 370 | 1 371 | 372 | 72, 16 373 | 374 | 375 | 376 | 0 377 | 378 | 379 | 380 | 381 | SelectProjectImage 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | wxLEFT|wxEXPAND 391 | 80 392 | 393 | 394 | wxHORIZONTAL 395 | 396 | wxTOP 397 | 3 398 | 399 | 400 | 401 | 1 402 | images\AddInDesktop48.png 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | wxHORIZONTAL 415 | 416 | wxEXPAND 417 | 0 418 | 419 | 420 | 421 | 422 | BeginDrag 423 | EndDrag 424 | DeleteItem 425 | SelChanged 426 | 427 | 200, 484 428 | 429 | 430 | 431 | wxEXPAND 432 | 0 433 | 434 | 435 | 436 | 437 | wxVERTICAL 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | wxEXPAND 453 | 4 454 | 455 | 456 | 457 | 458 | wxHORIZONTAL 459 | 460 | wxEXPAND 461 | 0 462 | 463 | 464 | 465 | 466 | 467 | 468 | wxALL 469 | 2 470 | 471 | 472 | 473 | 474 | OpenFolder 475 | 476 | 477 | 478 | 479 | wxALL 480 | 2 481 | 482 | 483 | 484 | 485 | SaveProject 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonbot/addin-wizard/eeaf8d8988a0c24d110918241f7bd03a24644c1a/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **NOTE**: This repository is no longer actively maintained as I am no longer an employee at Esri and do not have active access to Windows or any ArcGIS products. Hopefully you can get support at [Esri's official help forum](https://geonet.esri.com/groups/python-addins). 2 | 3 | # Python Add-In Wizard 4 | 5 | This is the application linked to in the help for creating Python Add-Ins in ArcGIS 10.1/10.2. It's a simple wxPython app bundled with py2exe, so it should act as a good example to learn from for doing that, too. 6 | 7 | This app does not require ArcGIS in any way to function, nor does it import or use arcpy/arcgisscripting in any way. It's just a UI to make boilerplate `.xml`/`.py` files. 8 | 9 | ## Licensing 10 | Copyright 2013 Esri 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | 24 | A copy of the license is available in the repository's license.txt file. 25 | 26 | [](Esri: ArcGIS Python Add-In Wizard) 27 | [](Esri Language: Python) 28 | -------------------------------------------------------------------------------- /addin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import datetime 3 | import itertools 4 | import random 5 | import operator 6 | import os 7 | import shutil 8 | import stat 9 | import time 10 | import uuid 11 | import xml.etree.ElementTree 12 | import xml.dom.minidom 13 | import _winreg 14 | 15 | NAMESPACE = "{http://schemas.esri.com/Desktop/AddIns}" 16 | 17 | def CURRENT_VERSION(): 18 | version_keys = (((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\ESRI\Desktop10.4"), "10.4"), 19 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\ESRI\Desktop10.4"), "10.4"), 20 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\ESRI\Desktop10.4"), "10.3"), 21 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\ESRI\Desktop10.3"), "10.3"), 22 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\ESRI\Desktop10.2"), "10.2"), 23 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\ESRI\Desktop10.2"), "10.2"), 24 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Wow6432Node\ESRI\Desktop10.1"), "10.1"), 25 | ((_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\ESRI\Desktop10.1"), "10.1")) 26 | for version_key, version_string in version_keys: 27 | try: 28 | _winreg.CloseKey(_winreg.OpenKey(*version_key)) 29 | return version_string 30 | except WindowsError: 31 | pass 32 | # Default to latest version 33 | return version_keys[0][1] 34 | 35 | def makeid(prefix="id", seen=set()): 36 | if prefix[-1].isdigit(): 37 | newnum = int(''.join(char for char in prefix if char.isdigit())) 38 | prefix = ''.join(char for char in prefix if not char.isdigit()) 39 | seen.add(newnum) 40 | num = 1 41 | while num in seen: 42 | num += 1 43 | seen.add(num) 44 | return prefix + str(num) 45 | 46 | class DelayedGetter(object): 47 | def __init__(self, id_to_get, id_cache): 48 | self._id = id_to_get 49 | self._cache = id_cache 50 | @property 51 | def item(self): 52 | return self._cache[self._id] 53 | 54 | class XMLSerializable(object): 55 | __registry__ = {} 56 | def xmlNode(self, parent_node): 57 | raise NotImplementedError("Method not implemented for {}".format( 58 | repr(self.__class__))) 59 | @classmethod 60 | def loadNode(cls, node, id_cache=None): 61 | tagname = node.tag[len(NAMESPACE):] 62 | if 'refID' in node.attrib: 63 | if id_cache and node.attrib['refID'] in id_cache: 64 | return id_cache[node.attrib['refID']] 65 | elif isinstance(id_cache, dict): 66 | return DelayedGetter(node.attrib['refID'], id_cache) 67 | elif tagname in cls.__registry__: 68 | return cls.__registry__[tagname].fromNode(node, id_cache) 69 | raise NotImplementedError( 70 | "Deserialization not implemented for {}".format(repr(cls))) 71 | @classmethod 72 | def registerType(cls, klass): 73 | cls.__registry__[klass.__name__] = klass 74 | return klass 75 | 76 | class XMLAttrMap(XMLSerializable): 77 | def addAttrMap(self, node): 78 | if hasattr(self, '__attr_map__'): 79 | for attr_node, attr_name in self.__attr_map__.iteritems(): 80 | value = getattr(self, attr_name, '') 81 | if isinstance(value, bool): 82 | value = str(value).lower() 83 | else: 84 | value = unicode(value) 85 | node.attrib[attr_node] = value 86 | @classmethod 87 | def fromNode(cls, node, id_cache=None): 88 | instance = cls() 89 | for attrib, mapping_attrib in getattr(cls, '__attr_map__', {}).iteritems(): 90 | val = node.attrib.get(attrib, '') 91 | if val in ('true', 'false'): # Coerce bool? 92 | val = True if val == 'true' else False 93 | else: # Coerce int? 94 | try: 95 | val = int(val) 96 | except ValueError: 97 | pass 98 | setattr(instance, mapping_attrib, val) 99 | if hasattr(instance, 'items'): 100 | item_nodes = node.find(NAMESPACE+"Items") 101 | for item in item_nodes.getchildren() if item_nodes is not None else []: 102 | instance.items.append(XMLSerializable.loadNode(item, id_cache)) 103 | if 'id' in node.attrib and isinstance(id_cache, dict): 104 | id_cache[node.attrib['id']] = instance 105 | makeid(node.attrib['id']) 106 | if 'class' in node.attrib: 107 | makeid(node.attrib['class']) 108 | help_node = node.find(NAMESPACE+"Help") 109 | if help_node is not None: 110 | instance.help_heading = help_node.attrib.get('heading', '') 111 | instance.help_string = (help_node.text or '').strip() 112 | return instance 113 | 114 | class HasPython(object): 115 | @property 116 | def python(self): 117 | methods = getattr(self, '__python_methods__', []) 118 | if hasattr(self, 'enabled_methods'): 119 | methods = [m for m in methods if m[0] in self.enabled_methods] 120 | method_string = "\n".join( 121 | " def {0}({1}):\n{2} pass".format(method, 122 | ", ".join(args), 123 | " {0}\n".format(repr(doc)) 124 | if doc 125 | else '') 126 | for method, doc, args in methods 127 | ) 128 | init_code = getattr(self, '__init_code__', '') 129 | if init_code: 130 | method_string = " def __init__(self):\n{0}\n{1}".format( 131 | "\n".join(" "+ line 132 | for line in init_code), 133 | method_string) 134 | if not method_string: 135 | method_string = " pass" 136 | comment_or_doc = ' # {0}'.format(self.__class__.__name__) 137 | if hasattr(self, 'id'): 138 | comment_or_doc = ' """Implementation for {0} ({1})"""'.format(self.id, 139 | self.__class__.__name__) 140 | return "class {0}(object):\n{1}\n{2}".format(self.klass, comment_or_doc, method_string) 141 | 142 | class RefID(object): 143 | def refNode(self, parent): 144 | return xml.etree.ElementTree.SubElement(parent, 145 | self.__class__.__name__, 146 | {'refID': self.id}) 147 | 148 | class Command(RefID): 149 | pass 150 | 151 | class UIControl(Command, XMLSerializable, HasPython): 152 | pass 153 | 154 | @XMLSerializable.registerType 155 | class Extension(XMLAttrMap, HasPython): 156 | "Extension" 157 | __attr_map__ = {'productName': 'name', 158 | 'name': 'name', 159 | 'description': 'description', 160 | 'class': 'klass', 161 | 'id': 'id', 162 | 'category': 'category', 163 | 'showInExtensionDialog': 'show_in_dialog', 164 | 'autoLoad': 'enabled'} 165 | __python_methods__ = [('startup', '', ['self']), 166 | ('shutdown', '', ['self']), 167 | ('activeViewChanged', '', ['self']), 168 | ('mapsChanged', '', ['self']), 169 | ('newDocument', '', ['self']), 170 | ('openDocument', '', ['self']), 171 | ('beforeCloseDocument', '', ['self']), 172 | ('closeDocument', '', ['self']), 173 | ('beforePageIndexExtentChange', '', ['self', 'old_id']), 174 | ('pageIndexExtentChanged', '', ['self', 'new_id']), 175 | ('contentsChanged', '', ['self']), 176 | ('spatialReferenceChanged', '', ['self']), 177 | ('itemAdded', '', ['self', 'new_item']), 178 | ('itemDeleted', '', ['self', 'deleted_item']), 179 | ('itemReordered', '', ['self', 'reordered_item', 'new_index']), 180 | ('onEditorSelectionChanged', '', ['self']), 181 | ('onCurrentLayerChanged', '', ['self']), 182 | ('onCurrentTaskChanged', '', ['self']), 183 | ('onStartEditing', '', ['self']), 184 | ('onStopEditing', '', ['self', 'save_changes']), 185 | ('onStartOperation', '', ['self']), 186 | ('beforeStopOperation', '', ['self']), 187 | ('onStopOperation', '', ['self']), 188 | ('onSaveEdits', '', ['self']), 189 | ('onChangeFeature', '', ['self']), 190 | ('onCreateFeature', '', ['self']), 191 | ('onDeleteFeature', '', ['self']), 192 | ('onUndo', '', ['self']), 193 | ('onRedo', '', ['self'])] 194 | @property 195 | def __init_code__(self): 196 | return ['# For optimal performance, please remove all unused methods ' 197 | 'in this class.', 198 | 199 | 'self.enabled = {}'.format(repr(self.enabled)), 200 | 201 | '# Starting in 10.,3, these attributes will be populated ' 202 | 'when edit events are triggered', 203 | 'self.editWorkspace = None # String with workspace path', 204 | 'self.currentLayer = None # arcpy.mapping.Layer of edit layer', 205 | 'self.currentFeature = None # arcpy.Geometry', 206 | 'self.editSelection = None # List of OIDs as ints'] 207 | def __init__(self, name=None, description=None, klass=None, id=None, category=None): 208 | self.name = name or 'New Extension' 209 | self.description = description or '' 210 | self.klass = klass or makeid("ExtensionClass") 211 | self.id = id or makeid("extension") 212 | self.category = category or '' 213 | self.show_in_dialog = True 214 | self.enabled = True 215 | self.enabled_methods = [] 216 | def xmlNode(self, parent): 217 | newnode = xml.etree.ElementTree.SubElement(parent, 218 | self.__class__.__name__) 219 | self.addAttrMap(newnode) 220 | return newnode 221 | 222 | class ControlContainer(XMLAttrMap): 223 | def __init__(self): 224 | self.items = [] 225 | def addItemsToNode(self, parent_node): 226 | elt = xml.etree.ElementTree.SubElement(parent_node, 'Items') 227 | for item in self.items: 228 | if hasattr(item, 'refNode'): 229 | item.refNode(elt) 230 | else: 231 | item.xmlNode(elt) 232 | def xmlNode(self, parent): 233 | newnode = xml.etree.ElementTree.SubElement(parent, 234 | self.__class__.__name__) 235 | self.addAttrMap(newnode) 236 | self.addItemsToNode(newnode) 237 | 238 | @XMLSerializable.registerType 239 | class Menu(ControlContainer, RefID): 240 | "Menu" 241 | __attr_map__ = {'caption': 'caption', 242 | 'isRootMenu': 'top_level', 243 | 'isShortcutMenu': 'shortcut_menu', 244 | 'separator': 'separator', 245 | 'category': 'category', 246 | 'id': 'id'} 247 | def __init__(self, caption='Menu', top_level=False, shortcut_menu=False, separator=False, category=None, id=None): 248 | super(Menu, self).__init__() 249 | self.caption = caption or '' 250 | self.top_level = bool(top_level) 251 | self.shortcut_menu = bool(shortcut_menu) 252 | self.separator = bool(separator) 253 | self.category = category or '' 254 | self.id = id or makeid("menuitem") 255 | 256 | @XMLSerializable.registerType 257 | class ToolPalette(ControlContainer, UIControl): 258 | "Tool Palette" 259 | __attr_map__ = {'columns': 'columns', 260 | 'canTearOff': 'tearoff', 261 | 'isMenuStyle': 'menu_style', 262 | 'category': 'category', 263 | 'id': 'id'} 264 | def __init__(self, caption=None, columns=2, tearoff=False, menu_style=False, category=None, id=None): 265 | super(ToolPalette, self).__init__() 266 | self.caption = caption or 'Palette' 267 | self.columns = columns 268 | self.tearoff = bool(tearoff) 269 | self.menu_style = bool(menu_style) 270 | self.category = category or '' 271 | self.id = id or makeid("tool_palette") 272 | 273 | @XMLSerializable.registerType 274 | class Toolbar(ControlContainer): 275 | "Toolbar" 276 | __attr_map__ = {'caption': 'caption', 277 | 'category': 'category', 278 | 'id': 'id', 279 | 'showInitially': 'show_initially'} 280 | def __init__(self, id=None, caption=None, category=None, show_initially=True): 281 | super(Toolbar, self).__init__() 282 | self.id = id or makeid("toolbar") 283 | self.caption = caption or 'Toolbar' 284 | self.category = category or '' 285 | self.show_initially = bool(show_initially) 286 | 287 | @XMLSerializable.registerType 288 | class Button(XMLAttrMap, UIControl): 289 | "Button" 290 | __python_methods__ = [('onClick', '', ['self'])] 291 | __init_code__ = ['self.enabled = True', 'self.checked = False'] 292 | __attr_map__ = {'caption': 'caption' , 293 | 'class': 'klass', 294 | 'category': 'category', 295 | 'image': 'image' , 296 | 'tip': 'tip' , 297 | 'message': 'message' , 298 | 'id': 'id', 299 | 'message': 'message'} 300 | def __init__(self, caption=None, klass=None, category=None, image=None, 301 | tip=None, message=None, id=None): 302 | self.caption = caption or "Button" 303 | self.klass = klass or makeid("ButtonClass") 304 | self.category = category or '' 305 | self.image = image or '' 306 | self.tip = tip or '' 307 | self.message = message or '' 308 | self.id = id or makeid("button") 309 | self.help_heading = '' 310 | self.help_string = '' 311 | def xmlNode(self, parent): 312 | newnode = xml.etree.ElementTree.SubElement(parent, 313 | self.__class__.__name__) 314 | self.addAttrMap(newnode) 315 | help = xml.etree.ElementTree.SubElement(newnode, 'Help', {'heading': self.help_heading}) 316 | help.text = self.help_string 317 | return newnode 318 | 319 | @XMLSerializable.registerType 320 | class ComboBox(Button): 321 | "Combo Box" 322 | __attr_map__ = {'caption': 'caption', 323 | 'category': 'category', 324 | 'id': 'id', 325 | 'class': 'klass', 326 | 'tip': 'tip', 327 | 'message': 'message', 328 | 'sizeString': 'size_string', 329 | 'itemSizeString': 'item_size_string', 330 | 'rows': 'rows', 331 | 'hint_text': 'hint'} 332 | @property 333 | def __init_code__(self): 334 | return ['self.items = ["item1", "item2"]', 335 | 'self.editable = {}'.format(repr(self.editable)), 336 | 'self.enabled = True', 337 | 'self.dropdownWidth = {}'.format(repr(self.item_size_string)), 338 | 'self.width = {}'.format(repr(self.size_string))] 339 | __python_methods__ = [('onSelChange', '', ['self', 'selection']), 340 | ('onEditChange', '', ['self', 'text']), 341 | ('onFocus', '', ['self', 'focused']), 342 | ('onEnter', '', ['self']), 343 | ('refresh', '', ['self']) 344 | ] 345 | def __init__(self, klass=None, id=None, category=None, caption=None): 346 | self.klass = klass or makeid("ComboBoxClass") 347 | self.id = id or makeid("combo_box") 348 | self.category = category or '' 349 | self.caption = caption or "ComboBox" 350 | self.message = '' 351 | self.tip = '' 352 | self.help_heading = '' 353 | self.help_string = '' 354 | self.size_string = "WWWWWW" 355 | self.item_size_string = "WWWWWW" 356 | self.hint_text = '' 357 | self.editable = True 358 | self.rows = 4 359 | 360 | @XMLSerializable.registerType 361 | class Tool(Button): 362 | "Python Tool" 363 | __init_code__ = ['self.enabled = True', 364 | 'self.shape = "NONE" # Can set to "Line", ' 365 | '"Circle" or "Rectangle" ' 366 | 'for interactive shape drawing ' 367 | 'and to activate the onLine/Polygon/Circle event sinks.'] 368 | __python_methods__ = [('onMouseDown', '', ['self', 'x', 'y', 'button', 'shift']), 369 | ('onMouseDownMap', '', ['self', 'x', 'y', 'button', 'shift']), 370 | ('onMouseUp', '', ['self', 'x', 'y', 'button', 'shift']), 371 | ('onMouseUpMap', '', ['self', 'x', 'y', 'button', 'shift']), 372 | ('onMouseMove', '', ['self', 'x', 'y', 'button', 'shift']), 373 | ('onMouseMoveMap', '', ['self', 'x', 'y', 'button', 'shift']), 374 | ('onDblClick', '', ['self']), 375 | ('onKeyDown', '', ['self', 'keycode', 'shift']), 376 | ('onKeyUp', '', ['self', 'keycode', 'shift']), 377 | ('deactivate', '', ['self']), 378 | ('onCircle', '', ['self', 'circle_geometry']), 379 | ('onLine', '', ['self', 'line_geometry']), 380 | ('onRectangle', '', ['self', 'rectangle_geometry']), 381 | ] 382 | def __init__(self, caption=None, klass=None, category=None, image=None, 383 | tip=None, message=None, id=None): 384 | super(Tool, self).__init__(caption, klass, category, image, tip, message, id) 385 | self.caption = caption or 'Tool' 386 | self.klass = klass or makeid("ToolClass") 387 | self.id = id or makeid("tool") 388 | self.image = image or '' 389 | self.help_heading = '' 390 | self.help_string = '' 391 | 392 | @XMLSerializable.registerType 393 | class MultiItem(XMLAttrMap, UIControl): 394 | "MultiItem" 395 | __attr_map__ = {'id': 'id', 396 | 'class': 'klass', 397 | 'hasSeparator': 'separator'} 398 | __python_methods__ = [('onItemClick', '', ['self'])] 399 | __init_code__ = ['self.items = ["item1", "item2"]'] 400 | def __init__(self, klass=None, id=None, separator=None): 401 | self.klass = klass or makeid("MultiItemClass") 402 | self.id = id or makeid("multi_item") 403 | self.caption = 'MultiItem' 404 | self.separator = bool(separator) 405 | def xmlNode(self, parent): 406 | newnode = xml.etree.ElementTree.SubElement(parent, 407 | self.__class__.__name__) 408 | self.addAttrMap(newnode) 409 | return newnode 410 | 411 | class PythonAddin(object): 412 | __apps__ = ('ArcMap', 'ArcCatalog', 'ArcGlobe', 'ArcScene') 413 | def __init__(self, name, description, namespace, author='Untitled', 414 | company='Untitled', version='0.1', image='', app='ArcMap', 415 | backup_files = False): 416 | self.name = name 417 | self.description = description 418 | self.namespace = namespace 419 | self.author = author 420 | self.company = company 421 | assert app in self.__apps__ 422 | self.app = app 423 | self.guid = "{{{}}}".format(uuid.uuid4()) 424 | self.version = version 425 | self.image = image 426 | self.addinfile = self.namespace + '.py' 427 | self.items = [] 428 | self.backup_files = backup_files 429 | self.last_backup = 0.0 430 | self.projectpath = '' 431 | def remove(self, target_item): 432 | def rm_(container_object, target_item): 433 | items = getattr(container_object, 'items', []) 434 | if target_item in items: 435 | container_object.items.pop(container_object.items.index(target_item)) 436 | return True 437 | else: 438 | for item in (i for i in items if isinstance(i, ControlContainer)): 439 | if rm_(item, target_item): 440 | return True 441 | return False 442 | return rm_(self, target_item) 443 | @property 444 | def commands(self): 445 | seen_ids = set() 446 | for command in self: 447 | if isinstance(command, UIControl) and command.id not in seen_ids: 448 | yield command 449 | seen_ids.add(command.id) 450 | @property 451 | def allmenus(self): 452 | seen_ids = set() 453 | for menu in self: 454 | if isinstance(menu, Menu) and menu.id not in seen_ids: 455 | yield menu 456 | seen_ids.add(menu.id) 457 | @property 458 | def menus(self): 459 | return [m for m in self.items if isinstance(m, Menu) and m.top_level] 460 | @property 461 | def toolbars(self): 462 | return [toolbar for toolbar in self.items if isinstance(toolbar, Toolbar)] 463 | @property 464 | def extensions(self): 465 | return [extension for extension in self.items if isinstance(extension, Extension)] 466 | @classmethod 467 | def fromXML(cls, xmlfile, backup_files = False): 468 | new_addin = cls('unknown', 'unknown', 'unknown') 469 | new_addin.backup_files = backup_files 470 | new_addin.last_backup = 0.0 471 | id_cache = {} 472 | doc = xml.etree.ElementTree.parse(xmlfile) 473 | root = doc.getroot() 474 | assert root.tag == NAMESPACE+'ESRI.Configuration', root.tag 475 | name_node = root.find(NAMESPACE+"Name") 476 | if name_node is None: 477 | name_node = root.find(NAMESPACE+"Name") 478 | if name_node is not None: 479 | new_addin.name = name_node.text.strip() 480 | else: 481 | new_addin.name = "Untitled" 482 | new_addin.guid = (root.find(NAMESPACE+"AddInID").text or '').strip() 483 | new_addin.description = (root.find(NAMESPACE+"Description").text or '').strip() 484 | new_addin.version = (root.find(NAMESPACE+"Version").text or '').strip() 485 | new_addin.image = (root.find(NAMESPACE+"Image").text or '').strip() 486 | new_addin.author = (root.find(NAMESPACE+"Author").text or '').strip() 487 | new_addin.company = (root.find(NAMESPACE+"Company").text or '').strip() 488 | addin_node = root.find(NAMESPACE+"AddIn") 489 | new_addin.addinfile = addin_node.attrib.get('library', '') 490 | new_addin.namespace = addin_node.attrib.get('namespace', '') 491 | projectpath = os.path.join(os.path.dirname(xmlfile), 'Install', os.path.dirname(new_addin.addinfile)) 492 | new_addin.projectpath = projectpath 493 | app_node = addin_node.getchildren()[0] 494 | new_addin.app = app_node.tag[len(NAMESPACE):] 495 | for command in app_node.find(NAMESPACE+"Commands").getchildren(): 496 | XMLSerializable.loadNode(command, id_cache) 497 | for tag in ("Extensions", "Toolbars", "Menus"): 498 | for item in app_node.find(NAMESPACE+tag).getchildren(): 499 | newobject = XMLSerializable.loadNode(item, id_cache) 500 | new_addin.items.append(newobject) 501 | def fix_references(addin_item, depth = 0): 502 | if hasattr(addin_item, 'items'): 503 | new_items = [i.item if isinstance(i, DelayedGetter) 504 | else i for i in addin_item.items] 505 | addin_item.items = new_items 506 | for item in new_items: 507 | fix_references(item, depth + 1) 508 | fix_references(new_addin) 509 | return new_addin 510 | def backup(self): 511 | addin_file = os.path.join(self.projectpath, self.addinfile) 512 | if (self.backup_files and os.path.isfile(addin_file) 513 | and os.stat(addin_file)[stat.ST_MTIME] > self.last_backup): 514 | path, ext = os.path.splitext(addin_file) 515 | new_index = 1 516 | new_file = path + "_" + str(new_index) + ext 517 | while os.path.exists(new_file): 518 | new_index += 1 519 | new_file = path + "_" + str(new_index) + ext 520 | if not hasattr(self, 'warning'): 521 | self.warning = u'' 522 | else: 523 | self.warning += "\n" 524 | self.warning += u"Python script {0} already exists. Saving a backup as {1}.".format(addin_file, new_file) 525 | shutil.copyfile(addin_file, new_file) 526 | self.last_backup = time.time() 527 | return self.addinfile 528 | def fixids(self, item = None, seen_ids = None): 529 | if seen_ids is None: 530 | seen_ids = {} 531 | target = self if item is None else item 532 | for thisattr in ('id', 'klass'): 533 | if hasattr(target, thisattr): 534 | if thisattr == 'id' and '.' not in getattr(target, thisattr, '') and '{' not in getattr(target, thisattr, ''): 535 | setattr(target, thisattr, self.namespace + "." + getattr(target, thisattr, thisattr)) 536 | if getattr(target, thisattr) in seen_ids and seen_ids[getattr(target, thisattr)] != target: 537 | if not hasattr(self, 'warning'): 538 | self.warning = '' 539 | else: 540 | self.warning += "\n" 541 | thisattrnum = 1 542 | while (getattr(target, thisattr) + "_" + str(thisattrnum)) in seen_ids: 543 | thisattrnum += 1 544 | newthisattr = getattr(target, thisattr) + "_" + str(thisattrnum) 545 | self.warning += u"{0} {1} is already in use. Renaming next usage to {2}.".format("Class" if thisattr == "klass" else "ID", 546 | getattr(target, thisattr), 547 | newthisattr) 548 | setattr(target, thisattr, newthisattr) 549 | seen_ids[getattr(target, thisattr)] = target 550 | if hasattr(target, 'items'): 551 | for subitem in target.items: 552 | self.fixids(subitem, seen_ids) 553 | @property 554 | def xml(self): 555 | root = xml.etree.ElementTree.Element('ESRI.Configuration', 556 | {'xmlns': "http://schemas.esri.com/Desktop/AddIns", 557 | 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance"}) 558 | xml.etree.ElementTree.SubElement(root, 'Name').text = self.name 559 | xml.etree.ElementTree.SubElement(root, 'AddInID').text = self.guid 560 | xml.etree.ElementTree.SubElement(root, 'Description').text = self.description 561 | xml.etree.ElementTree.SubElement(root, 'Version').text = self.version 562 | xml.etree.ElementTree.SubElement(root, 'Image').text = self.image 563 | xml.etree.ElementTree.SubElement(root, 'Author').text = self.author 564 | xml.etree.ElementTree.SubElement(root, 'Company').text = self.company 565 | now = datetime.datetime.now().strftime("%m/%d/%Y") 566 | xml.etree.ElementTree.SubElement(root, 'Date').text = now 567 | targets = xml.etree.ElementTree.SubElement(root, 'Targets') 568 | arcgis_version = CURRENT_VERSION() 569 | target = xml.etree.ElementTree.SubElement(targets, 570 | 'Target', 571 | {'name': "Desktop", 572 | 'version': arcgis_version}) 573 | addinnode = xml.etree.ElementTree.SubElement(root, 574 | 'AddIn', 575 | {'language': 'PYTHON', 576 | 'library': self.addinfile, 577 | 'namespace': 578 | self.namespace}) 579 | 580 | self.fixids() 581 | 582 | appnode = xml.etree.ElementTree.SubElement(addinnode, self.app) 583 | commandnode = xml.etree.ElementTree.SubElement(appnode, 'Commands') 584 | for command in self.commands: 585 | command.xmlNode(commandnode) 586 | extensionnode = xml.etree.ElementTree.SubElement(appnode, 'Extensions') 587 | for extension in self.extensions: 588 | extension.xmlNode(extensionnode) 589 | toolbarnode = xml.etree.ElementTree.SubElement(appnode, 'Toolbars') 590 | for toolbar in self.toolbars: 591 | toolbar.xmlNode(toolbarnode) 592 | menunode = xml.etree.ElementTree.SubElement(appnode, 'Menus') 593 | for menu in self.allmenus: 594 | menu.xmlNode(menunode) 595 | markup = xml.etree.ElementTree.tostring(root).encode("utf-8") 596 | return xml.dom.minidom.parseString(markup).toprettyxml(" ") 597 | def __iter__(self): 598 | def ls_(item): 599 | if hasattr(item, 'items'): 600 | for item_ in item.items: 601 | for item__ in ls_(item_): 602 | yield item__ 603 | yield item 604 | for bitem in self.items: 605 | for aitem in ls_(bitem): 606 | yield aitem 607 | @property 608 | def python(self): 609 | return ("# coding: utf-8\nimport arcpy\nimport pythonaddins\n\n" + 610 | "\n\n".join( 611 | sorted( 612 | set(x.python 613 | for x in self 614 | if hasattr(x, 'python'))))) 615 | 616 | class PythonAddinProjectDirectory(object): 617 | def __init__(self, path, backup_files = False): 618 | self._path = path 619 | self.backup_files = backup_files 620 | if not os.path.exists(path): 621 | raise IOError(u"{0} does not exist. Please select a " 622 | u"directory that exists.".format(path)) 623 | listing = os.listdir(path) 624 | if listing: 625 | if not all(item in listing for item in ('config.xml', 626 | 'Install', 627 | 'Images')): 628 | raise ValueError(u"{0} is not empty. Please select an empty " 629 | u"directory to host this new " 630 | u"addin.".format(path)) 631 | else: 632 | self.addin = PythonAddin.fromXML(os.path.join(self._path, 633 | 'config.xml'), 634 | backup_files) 635 | self.warning = getattr(self.addin, 'warning', None) 636 | else: 637 | # Fix for NIM092018 638 | addin_name = os.path.basename(path) 639 | self.addin = PythonAddin("Python Addin", 640 | "New Addin", 641 | (addin_name if addin_name 642 | else 'python') + "_addin", 643 | backup_files = True) 644 | def save(self): 645 | # Make install/images dir 646 | install_dir = os.path.join(self._path, 'Install') 647 | images_dir = os.path.join(self._path, 'Images') 648 | if not os.path.exists(install_dir): 649 | os.mkdir(install_dir) 650 | if not os.path.exists(images_dir): 651 | os.mkdir(images_dir) 652 | 653 | # Copy packaging\* into project dir 654 | initial_dirname = os.path.dirname(os.path.abspath(__file__)) 655 | if os.path.isfile(initial_dirname): 656 | initial_dirname = os.path.dirname(initial_dirname) 657 | packaging_dir = os.path.join(initial_dirname, 658 | 'packaging') 659 | for filename in os.listdir(packaging_dir): 660 | if not os.path.exists(os.path.join(self._path, filename)): 661 | shutil.copyfile(os.path.join(packaging_dir, 662 | filename), 663 | os.path.join(self._path, filename)) 664 | 665 | # For consolidating images 666 | seen_images = set([self.addin.image]) 667 | 668 | # Auto-fill category 669 | for item in self.addin: 670 | # Auto-category 671 | if hasattr(item, 'category'): 672 | item.category = self.addin.name or self.addin.description 673 | # Collect for later 674 | if getattr(item, 'image', ''): 675 | seen_images.add(item.image) 676 | 677 | # Consolidate images 678 | seen_images = filter(bool, seen_images) 679 | full_path = dict((image, 680 | os.path.abspath( 681 | os.path.join(self._path, image))) 682 | for image in seen_images) 683 | to_relocate = set(image_file 684 | for (image_name, image_file) in full_path.iteritems() 685 | if os.path.dirname(image_file) != images_dir) 686 | relocated_images = {} 687 | image_files = set(x.lower() for x in os.listdir(images_dir)) 688 | for image_file in to_relocate: 689 | new_filename = os.path.basename(image_file) 690 | if new_filename.lower() in image_files: 691 | num = 1 692 | fn, format = os.path.splitext(new_filename) 693 | while new_filename.lower() in image_files: 694 | new_filename = fn + "_" + str(num) + format 695 | num += 1 696 | relocated_images[image_file] = new_filename 697 | shutil.copyfile(image_file, os.path.join(self._path, 698 | 'Images', 699 | new_filename)) 700 | for item_with_image in (item 701 | for item in ([self.addin] + list(self.addin)) 702 | if getattr(item, 'image', '')): 703 | item_image = item_with_image.image 704 | if item_image in relocated_images: 705 | item_with_image.image = os.path.join('Images', 706 | relocated_images[ 707 | item_image]) 708 | else: 709 | item_with_image.image = os.path.join('Images', 710 | os.path.basename( 711 | item_image)) 712 | 713 | # Back up .py file if necessary 714 | filename = self.addin.backup() 715 | 716 | # Output XML and Python stub 717 | with open(os.path.join(self._path, 'config.xml'), 'wb') as out_handle: 718 | out_handle.write(self.addin.xml) 719 | self.addin.last_backup = time.time() 720 | with open(os.path.join(install_dir, filename), 'wb') as out_python: 721 | out_python.write(self.addin.python) 722 | self.addin.last_save = time.time() 723 | 724 | if __name__ == "__main__": 725 | myaddin = PythonAddin("My Addin", "This is a new addin", "myaddin") 726 | toolbar = Toolbar() 727 | top_menu = Menu("Top Level Menu", id="top_menu") 728 | top_menu.items.append(Button("Top Menu button 1", id="tb1")) 729 | top_menu.items.append(Button("Top Menu button 2", id="tb2")) 730 | menu = Menu("Embedded Menu", id="embedded_menu") 731 | menu.items.append(Button("Menu button", "MenuButton")) 732 | palette = ToolPalette("Awesome Palette") 733 | palette.items.append(Tool("New Fun Tool", "NewFunTool")) 734 | palette.items.append(Tool("New Not Fun Tool", "NewNotFunTool")) 735 | toolbar.items.append(Button("Hello there", "HelloButton")) 736 | toolbar.items.append(menu) 737 | toolbar.items.append(palette) 738 | myaddin.items.append(toolbar) 739 | myaddin.items.append(top_menu) 740 | print myaddin.xml 741 | -------------------------------------------------------------------------------- /addin_assistant.pyw: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import addin 3 | import addin_ui 4 | import os 5 | import re 6 | import sys 7 | import traceback 8 | import wx 9 | 10 | from i18n import _ 11 | 12 | class AddinMakerAppWindow(addin_ui.AddinMakerWindow): 13 | # Sentinel/singletons for the treeview 14 | class _extensiontoplevel(object): 15 | "Extensions" 16 | pass 17 | class _menutoplevel(object): 18 | "Toplevel Menus" 19 | pass 20 | class _toolbartoplevel(object): 21 | "Toolbars" 22 | pass 23 | def __init__(self, *args, **kws): 24 | super(AddinMakerAppWindow, self).__init__(*args, **kws) 25 | self.Bind(wx.EVT_CLOSE, self.OnClose) 26 | 27 | self._selected_data = None 28 | self.contents_tree.Bind(wx.EVT_RIGHT_DOWN, self.TreePopupRClick) 29 | self.contents_tree.Bind(wx.EVT_CONTEXT_MENU, self.TreePopup) 30 | 31 | self._newextensionid = wx.NewId() 32 | self._newmenuid = wx.NewId() 33 | self._newtoolbarid = wx.NewId() 34 | 35 | def Show(self): 36 | super(AddinMakerAppWindow, self).Show() 37 | self.SelectFolder(None) 38 | 39 | def AddExtension(self, event): 40 | extension = addin.Extension() 41 | self.project.addin.items.append(extension) 42 | menuitem = self.contents_tree.AppendItem(self.extensionsroot, extension.name) 43 | self.contents_tree.SetItemPyData(menuitem, extension) 44 | self.contents_tree.SelectItem(menuitem, True) 45 | self.save_button.Enable(True) 46 | 47 | def AddMenu(self, event): 48 | menu = addin.Menu("Menu", self._selected_data is self._menutoplevel, 49 | id=self.project.addin.namespace+'.menu') 50 | self.project.addin.items.append(menu) 51 | menuitem = self.contents_tree.AppendItem(self.menusroot, menu.caption) 52 | self.contents_tree.SetItemPyData(menuitem, menu) 53 | self.contents_tree.SelectItem(menuitem, True) 54 | self.save_button.Enable(True) 55 | 56 | def AddToolbar(self, event): 57 | toolbar = addin.Toolbar(id=self.project.addin.namespace+'.toolbar') 58 | self.project.addin.items.append(toolbar) 59 | toolbaritem = self.contents_tree.AppendItem(self.toolbarsroot, toolbar.caption) 60 | self.contents_tree.SetItemPyData(toolbaritem, toolbar) 61 | self.contents_tree.SelectItem(toolbaritem, True) 62 | self.save_button.Enable(True) 63 | 64 | @property 65 | def extensionmenu(self): 66 | extensionmenu = wx.Menu() 67 | cmd = extensionmenu.Append(self._newextensionid, _("New Extension")) 68 | extensionmenu.Bind(wx.EVT_MENU, self.AddExtension, cmd) 69 | return extensionmenu 70 | 71 | @property 72 | def menumenu(self): 73 | menumenu = wx.Menu() 74 | cmd = menumenu.Append(self._newmenuid, _("New Menu")) 75 | menumenu.Bind(wx.EVT_MENU, self.AddMenu, cmd) 76 | return menumenu 77 | 78 | @property 79 | def toolbarmenu(self): 80 | toolbarmenu = wx.Menu() 81 | cmd = toolbarmenu.Append(self._newtoolbarid, _("New Toolbar")) 82 | toolbarmenu.Bind(wx.EVT_MENU, self.AddToolbar, cmd) 83 | return toolbarmenu 84 | 85 | @property 86 | def controlcontainermenu(self): 87 | class ItemAppender(object): 88 | def __init__(self, tree, selection, item, cls, save_button, namespace): 89 | self.tree = tree 90 | self.selection = selection 91 | self.item = item 92 | self.cls = cls 93 | self.save_button = save_button 94 | self.namespace = namespace 95 | def __call__(self, event): 96 | try: 97 | new_item = self.cls(id=self.namespace+'.'+self.cls.__name__.lower()) 98 | self.item.items.append(new_item) 99 | toolbaritem = self.tree.AppendItem(self.selection, getattr(new_item, 'caption', unicode(new_item))) 100 | self.tree.SetItemPyData(toolbaritem, new_item) 101 | self.tree.SelectItem(toolbaritem, True) 102 | self.save_button.Enable(True) 103 | except Exception as e: 104 | traceback.print_exc() 105 | print e 106 | 107 | tree = self.contents_tree 108 | selection = self.contents_tree.GetSelection() 109 | item = self._selected_data 110 | 111 | controlcontainermenu = wx.Menu() 112 | if not isinstance(item, addin.ToolPalette): 113 | buttoncmd = controlcontainermenu.Append(-1, _("New Button")) 114 | controlcontainermenu.Bind(wx.EVT_MENU, ItemAppender(tree, selection, item, addin.Button, self.save_button, self.project.addin.namespace), buttoncmd) 115 | menucmd = controlcontainermenu.Append(-1, _("New Menu")) 116 | controlcontainermenu.Bind(wx.EVT_MENU, ItemAppender(tree, selection, item, addin.Menu, self.save_button, self.project.addin.namespace), menucmd) 117 | if isinstance(item, addin.Menu): 118 | pass 119 | #multiitemcmd = controlcontainermenu.Append(-1, "New MultiItem") 120 | #controlcontainermenu.Bind(wx.EVT_MENU, 121 | # ItemAppender(tree, selection, item, 122 | # addin.MultiItem, self.save_button, self.project.addin.namespace), 123 | # multiitemcmd) 124 | else: 125 | toolcmd = controlcontainermenu.Append(-1, _("New Tool")) 126 | controlcontainermenu.Bind(wx.EVT_MENU, ItemAppender(tree, selection, item, addin.Tool, self.save_button, self.project.addin.namespace), toolcmd) 127 | if not isinstance(item, (addin.ToolPalette, addin.Menu)): 128 | palettecmd = controlcontainermenu.Append(-1, _("New Tool Palette")) 129 | controlcontainermenu.Bind(wx.EVT_MENU, ItemAppender(tree, selection, item, addin.ToolPalette, self.save_button, self.project.addin.namespace), palettecmd) 130 | if isinstance(item, addin.Toolbar): 131 | comboboxcmd = controlcontainermenu.Append(-1, _("New Combo Box")) 132 | controlcontainermenu.Bind(wx.EVT_MENU, ItemAppender(tree, selection, item, addin.ComboBox, self.save_button, self.project.addin.namespace), comboboxcmd) 133 | return controlcontainermenu 134 | 135 | def loadTreeView(self): 136 | def populate(parent, item): 137 | child = self.contents_tree.AppendItem(parent, 138 | unicode(getattr(item, 139 | 'caption', 140 | getattr(item, 141 | 'name', 142 | unicode(item))))) 143 | self.contents_tree.SetItemPyData(child, item) 144 | if hasattr(item, 'items'): 145 | for child_item in item.items: 146 | populate(child, child_item) 147 | self.contents_tree.Expand(parent) 148 | # Set up treeview control 149 | self.contents_tree.DeleteAllItems() 150 | self.treeroot = self.contents_tree.AddRoot("Root") 151 | self.extensionsroot = self.contents_tree.AppendItem(self.treeroot, 152 | _("EXTENSIONS")) 153 | self.contents_tree.SetItemPyData(self.extensionsroot, 154 | self._extensiontoplevel) 155 | for item in self.project.addin.extensions: 156 | populate(self.extensionsroot, item) 157 | self.menusroot = self.contents_tree.AppendItem(self.treeroot, 158 | _("MENUS")) 159 | self.contents_tree.SetItemPyData(self.menusroot, 160 | self._menutoplevel) 161 | for item in self.project.addin.menus: 162 | populate(self.menusroot, item) 163 | self.toolbarsroot = self.contents_tree.AppendItem(self.treeroot, 164 | _("TOOLBARS")) 165 | self.contents_tree.SetItemPyData(self.toolbarsroot, 166 | self._toolbartoplevel) 167 | for item in self.project.addin.toolbars: 168 | populate(self.toolbarsroot, item) 169 | self.contents_tree.SetItemBold(self.extensionsroot, True) 170 | self.contents_tree.SetItemBold(self.menusroot, True) 171 | self.contents_tree.SetItemBold(self.toolbarsroot, True) 172 | 173 | def loadProject(self): 174 | if getattr(self.project, 'warning', None): 175 | msgdlg = wx.MessageDialog(self, unicode(self.project.warning), 176 | _("Project Information"), 177 | wx.OK | wx.ICON_INFORMATION) 178 | msgdlg.ShowModal() 179 | msgdlg.Destroy() 180 | del self.project.warning 181 | # Repopulate the tree view control et al with settings from the loaded project 182 | self.loadTreeView() 183 | # Product selection 184 | self.product_combo_box.SetStringSelection(self.project.addin.app) 185 | # Set up metadata text entry 186 | self.project_name.SetLabel(self.project.addin.name) 187 | self.project_version.SetLabel(self.project.addin.version) 188 | self.project_company.SetLabel(self.project.addin.company) 189 | self.project_description.SetLabel(self.project.addin.description) 190 | self.project_author.SetLabel(self.project.addin.author) 191 | self.updateProjectImage() 192 | 193 | def OnClose(self, event): 194 | if self.save_button.IsEnabled(): 195 | confirmdlg = wx.MessageDialog(self, _("Save your changes before exiting?"), 196 | _("Save before exiting?"), 197 | wx.YES_NO | wx.ICON_QUESTION) 198 | res = confirmdlg.ShowModal() 199 | confirmdlg.Destroy() 200 | if res == wx.ID_YES: 201 | self.SaveProject(event) 202 | self.Destroy() 203 | 204 | @property 205 | def _path_file(self): 206 | return os.path.join((os.environ.get('APPDATA', '') or 207 | os.path.expanduser('~')), 208 | '.ESRI.PythonAddinMaker.Path') 209 | @property 210 | def lastPath(self): 211 | try: 212 | if os.path.isfile(self._path_file): 213 | new_path = open(self._path_file, 'rb').read() 214 | if os.path.isdir(new_path): 215 | return new_path 216 | except: 217 | pass 218 | return wx.StandardPaths.Get().GetDocumentsDir() 219 | @lastPath.setter 220 | def lastPath(self, new_value): 221 | try: 222 | with open(self._path_file, 'wb') as out_path: 223 | out_path.write(new_value) 224 | except: 225 | pass 226 | def SelectFolder(self, event): 227 | import traceback 228 | dlg = wx.DirDialog(self, _("Choose a directory to use as an Add-In project root:"), 229 | style=wx.DD_DEFAULT_STYLE) 230 | dlg.SetPath(self.lastPath) 231 | if dlg.ShowModal() == wx.ID_OK: 232 | self.path = dlg.GetPath() 233 | try: 234 | self.project = addin.PythonAddinProjectDirectory(self.path, True) 235 | self.lastPath = self.path 236 | except Exception as e: 237 | traceback.print_exc() 238 | print repr(e.message) 239 | errdlg = wx.MessageDialog(self, e.message, 240 | _("Error initializing Add-In"), 241 | wx.OK | wx.ICON_ERROR) 242 | errdlg.ShowModal() 243 | errdlg.Destroy() 244 | traceback.print_exc() 245 | else: 246 | self.folder_button.SetLabel(self.path) 247 | self.loadProject() 248 | return 249 | self.SelectFolder(event) 250 | elif event is None: 251 | sys.exit(0) 252 | else: 253 | return 254 | 255 | def ProjectNameText(self, event): 256 | newvalue = self.project_name.GetLabel().strip() 257 | if newvalue: 258 | self.project.addin.name = newvalue 259 | self.project_name.SetBackgroundColour('White') 260 | else: 261 | self.project_name.SetBackgroundColour('#FF8080') 262 | self.Refresh() 263 | self.save_button.Enable(True) 264 | event.Skip() 265 | 266 | def ProjectCompanyText(self, event): 267 | newvalue = self.project_company.GetLabel() 268 | self.project.addin.company = newvalue 269 | self.save_button.Enable(True) 270 | event.Skip() 271 | 272 | def ProjectDescriptionText(self, event): 273 | newvalue = self.project_description.GetLabel() 274 | self.project.addin.description = newvalue 275 | self.save_button.Enable(True) 276 | event.Skip() 277 | 278 | def ProjectAuthorText(self, event): 279 | newvalue = self.project_author.GetLabel() 280 | self.project.addin.author = newvalue 281 | self.save_button.Enable(True) 282 | event.Skip() 283 | 284 | def ProjectVersionText(self, event): 285 | newvalue = self.project_version.GetLabel().strip() 286 | if newvalue: 287 | self.project.addin.version = newvalue 288 | self.project_version.SetBackgroundColour('White') 289 | else: 290 | self.project_version.SetBackgroundColour('#FF8080') 291 | self.Refresh() 292 | self.save_button.Enable(True) 293 | event.Skip() 294 | 295 | def ComboBox(self, event): 296 | self.project.addin.app = self.product_combo_box.GetValue() 297 | self.save_button.Enable(True) 298 | event.Skip() 299 | 300 | def setupPropsDialog(self): 301 | sizer = self.item_property_panel.GetSizer() 302 | sizer.Clear(True) 303 | if hasattr(self._selected_data, '__doc__') and self._selected_data.__doc__: 304 | st = wx.StaticText(self.item_property_panel, -1, _(self._selected_data.__doc__)) 305 | st.SetFont(wx.Font(16, wx.DEFAULT, wx.NORMAL, wx.NORMAL, 0, "")) 306 | sizer.Add(st, 0, wx.ALL, 8) 307 | pythonliteral = re.compile("^[_A-Za-z][_A-Za-z0-9]*$").match 308 | namespacedpythonliteral = re.compile("^([_A-Za-z][_A-Za-z0-9]*[.])*[_A-Za-z][_A-Za-z0-9]+$").match 309 | def isinteger(val): 310 | try: 311 | int(val) 312 | return True 313 | except: 314 | return False 315 | def nonemptystring(val): 316 | return bool((val or '').strip()) 317 | class Namespacer(object): 318 | def __init__(self, project, control): 319 | self.project = project 320 | self.control = control 321 | def __call__(self, event): 322 | oldlabel = self.control.GetLabel() 323 | if '.' not in oldlabel: 324 | newlabel = self.project.addin.namespace + '.' + oldlabel 325 | self.control.SetLabel(newlabel) 326 | proplist = [p for p in (('name', _("Name"), unicode, nonemptystring, None), 327 | ('caption', _("Caption"), unicode, nonemptystring, None), 328 | ('klass', _("Class Name"), unicode, pythonliteral, None), 329 | ('id', _("ID (Variable Name)"), unicode, namespacedpythonliteral, Namespacer), 330 | ('description', _("Description"), unicode, None, None), 331 | ('tip', _("Tooltip"), unicode, None, None), 332 | ('message', _("Message"), unicode, None, None), 333 | ('hint_text', _("Hint Text"), unicode, None, None), 334 | ('help_heading', _("Help Heading"), unicode, None, None), 335 | ('help_string', _("Help Content"), unicode, None, None), 336 | ('enabled_methods', _("Methods to Implement"), list, None, None), 337 | ('separator', _("Has Separator"), bool, None, None), 338 | ('show_initially', _("Show Initially"), bool, None, None), 339 | ('enabled', _("Load Automatically"), bool, None, None), 340 | ('menu_style', _("Menu Style"), bool, None, None), 341 | ('shortcut_menu', _("Is Shortcut Menu"), bool, None, None), 342 | ('columns', _("Column Count"), unicode, isinteger, None), 343 | ('image', _("Image for Control"), wx.Bitmap, None, None)) 344 | if hasattr(self._selected_data, p[0])] 345 | for prop, caption, datatype, validator, fixer in proplist: 346 | # This is all kind of hairy, sorry 347 | newsizer = wx.BoxSizer(wx.HORIZONTAL) 348 | # Text entry 349 | if datatype in (unicode, int): 350 | st = wx.StaticText(self.item_property_panel, -1, caption + ":", style=wx.ALIGN_RIGHT) 351 | st.SetMinSize((175, 16)) 352 | newsizer.Add(st, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) 353 | text = wx.TextCtrl(self.item_property_panel, -1, unicode(getattr(self._selected_data, prop, '')) or '') 354 | text.SetBackgroundColour('White') 355 | class edittext(object): 356 | def __init__(self, edit_object, command, app, propname, validator, datatype): 357 | self.edit_object = edit_object 358 | self.command = command 359 | self.app = app 360 | self.propname = propname 361 | self.validator = validator 362 | self.datatype = datatype 363 | def __call__(self, event): 364 | newvalue = self.command.GetLabel() 365 | if self.validator is None or self.validator(newvalue): 366 | try: 367 | setattr(self.edit_object, self.propname, self.datatype(newvalue)) 368 | except Exception as e: 369 | traceback.print_exc() 370 | print e 371 | self.app.contents_tree.SetItemText(self.app.contents_tree.GetSelection(), 372 | unicode(getattr(self.edit_object, 'caption', 373 | getattr(self.edit_object, 'name', 374 | unicode(self.edit_object))))) 375 | self.app.save_button.Enable(True) 376 | self.command.SetBackgroundColour('White') 377 | self.app.Refresh() 378 | else: 379 | self.command.SetBackgroundColour('#FF8080') 380 | self.app.Refresh() 381 | event.Skip() 382 | self.Bind(wx.EVT_TEXT, edittext(self._selected_data, text, self, prop, validator, datatype), text) 383 | if fixer: 384 | newfixer = fixer(self.project, text) 385 | text.Bind(wx.EVT_SET_FOCUS, newfixer) 386 | text.Bind(wx.EVT_KILL_FOCUS, newfixer) 387 | newsizer.Add(text, 1, wx.RIGHT, 8) 388 | # Checkbox 389 | elif datatype is bool: 390 | class toggle(object): 391 | def __init__(self, edit_object, propname, control, app): 392 | self.edit_object = edit_object 393 | self.propname = propname 394 | self.control = control 395 | self.app = app 396 | def __call__(self, event): 397 | setattr(self.edit_object, self.propname, self.control.GetValue()) 398 | self.app.save_button.Enable(True) 399 | boolcheck = wx.CheckBox(self.item_property_panel, -1, caption) 400 | boolcheck.SetValue(getattr(self._selected_data, prop)) 401 | self.Bind(wx.EVT_CHECKBOX, toggle(self._selected_data, prop, boolcheck, self), boolcheck) 402 | newsizer.Add(boolcheck, 1, wx.LEFT, 178) 403 | # Image selection 404 | elif datatype is wx.Bitmap: 405 | class pickbitmap(object): 406 | def __init__(self, edit_object, propname, control, app): 407 | self.edit_object = edit_object 408 | self.propname = propname 409 | self.control = control 410 | self.app = app 411 | def __call__(self, event): 412 | images_path = os.path.join(self.app.path, 'Images') 413 | potentialdir = getattr(self.edit_object, self.propname, '') 414 | if potentialdir: 415 | images_path = os.path.join(self.app.path, 416 | os.path.dirname(potentialdir)) 417 | default_path = (images_path 418 | if os.path.exists(images_path) 419 | else self.app.path) 420 | dlg = wx.FileDialog( 421 | self.app, message=_("Choose an image file for control"), 422 | defaultDir=default_path, 423 | defaultFile="", 424 | wildcard=(_("PNG images (*.png)")+'|*.png|'+ 425 | _("GIF images (*.gif)")+'|*.gif|'+ 426 | _("BMP images (*.bmp)")+'|*.bmp'), 427 | style=wx.OPEN) 428 | if dlg.ShowModal() == wx.ID_OK: 429 | image_file = dlg.GetPath() 430 | bitmap = wx.Bitmap(image_file, wx.BITMAP_TYPE_ANY) 431 | self.app.save_button.Enable(True) 432 | self.control.SetBitmapLabel(bitmap) 433 | setattr(self.edit_object, self.propname, image_file) 434 | self.app.Fit() 435 | self.app.Layout() 436 | self.app.Refresh() 437 | 438 | st = wx.StaticText(self.item_property_panel, -1, _("Image for control:"), style=wx.ALIGN_RIGHT) 439 | st.SetMinSize((175, 16)) 440 | newsizer.Add(st, 0, wx.ALL|wx.ALIGN_TOP, 2) 441 | if getattr(self._selected_data, prop, ''): 442 | bitmap = wx.Bitmap(os.path.join(self.path, getattr(self._selected_data, prop)), wx.BITMAP_TYPE_ANY) 443 | else: 444 | bitmap = wx.EmptyBitmap(16, 16, 32) 445 | bitmap.SetMaskColour((0, 0, 0)) 446 | if bitmap.HasAlpha(): 447 | print "ALPHA" 448 | for xc in xrange(bitmap.GetWidth()): 449 | for yc in xrange(bitmap.GetHeight()): 450 | bitmap.SetAlpha(xc, yc, 255) 451 | choosefilebutton = wx.BitmapButton(self.item_property_panel, -1, bitmap) 452 | self.Bind(wx.EVT_BUTTON, pickbitmap(self._selected_data, prop, choosefilebutton, self), choosefilebutton) 453 | newsizer.Add(choosefilebutton, 1, wx.ALL|wx.EXPAND, 0) 454 | # Check list for enabled methods 455 | elif datatype is list: 456 | st = wx.StaticText(self.item_property_panel, -1, caption + ":", style=wx.ALIGN_RIGHT) 457 | st.SetMinSize((175, 16)) 458 | newsizer.Add(st, 0, wx.ALL|wx.ALIGN_TOP, 2) 459 | 460 | clb = wx.CheckListBox(self.item_property_panel, -1) 461 | clb.Set([x[0] for x in self._selected_data.__python_methods__]) 462 | clb.SetCheckedStrings(self._selected_data.enabled_methods) 463 | class Checker(object): 464 | def __init__(self, data, ctrl, app): 465 | self._data = data 466 | self._ctrl = ctrl 467 | self._app = app 468 | def __call__(self, event): 469 | index = event.GetSelection() 470 | method = self._ctrl.GetString(index) 471 | if self._ctrl.IsChecked(index): 472 | if method not in self._data.enabled_methods: 473 | self._data.enabled_methods.append(method) 474 | else: 475 | if method in self._data.enabled_methods: 476 | self._data.enabled_methods.pop(self._data.enabled_methods.index(method)) 477 | self._app.save_button.Enable(True) 478 | checker = Checker(self._selected_data, clb, self) 479 | self.Bind(wx.EVT_CHECKLISTBOX, checker, clb) 480 | newsizer.Add(clb, 1, wx.RIGHT|wx.EXPAND, 8) 481 | 482 | # WHO KNOWS! 483 | else: 484 | newsizer.Add(wx.StaticText(self.item_property_panel, -1, caption + ": " + unicode(getattr(self._selected_data, prop))), 0, wx.EXPAND) 485 | sizer.Add(newsizer, 0, wx.EXPAND|wx.BOTTOM, 8) 486 | sizer.Layout() 487 | self.Refresh() 488 | 489 | def SelChanged(self, event): 490 | try: 491 | self._selected_data = self.contents_tree.GetItemPyData(self.contents_tree.GetSelection()) 492 | except: 493 | self._selected_data = None 494 | self.setupPropsDialog() 495 | 496 | def DeleteItem(self, event): 497 | pass 498 | 499 | def ChangeTab(self, event): 500 | pass 501 | 502 | def SelectProjectImage(self, event): 503 | dlg = wx.FileDialog( 504 | self, message=_("Choose an image file"), 505 | defaultDir=(os.path.join(self.path, 506 | os.path.dirname(self.project.addin.image)) 507 | if self.project.addin.image 508 | else self.path), 509 | defaultFile="", 510 | wildcard=(_("PNG images (*.png)")+u'|*.png|'+ 511 | _("GIF images (*.gif)")+u'|*.gif|'+ 512 | _("BMP images (*.bmp)")+u'|*.bmp'), 513 | style=wx.OPEN) 514 | if dlg.ShowModal() == wx.ID_OK: 515 | image_file = dlg.GetPath() 516 | self.project.addin.image = image_file 517 | try: 518 | self.updateProjectImage() 519 | except Exception as e: 520 | print "IMAGE LOAD", e 521 | self.save_button.Enable(True) 522 | event.Skip() 523 | 524 | def updateProjectImage(self): 525 | if self.project.addin.image: 526 | image_file = os.path.join(self.project._path, self.project.addin.image) 527 | bitmap = wx.Bitmap(image_file, wx.BITMAP_TYPE_ANY) 528 | self.icon_bitmap.SetBitmap(bitmap) 529 | self.Fit() 530 | self.Refresh() 531 | 532 | def SaveProject(self, event): 533 | try: 534 | print self.project.addin.xml 535 | print 536 | print self.project.addin.python 537 | self.project.save() 538 | if getattr(self.project.addin, 'warning', None): 539 | msgdlg = wx.MessageDialog(self, unicode(self.project.addin.warning), 540 | _("Project Information"), 541 | wx.OK | wx.ICON_INFORMATION) 542 | msgdlg.ShowModal() 543 | msgdlg.Destroy() 544 | del self.project.addin.warning 545 | self.save_button.Enable(False) 546 | self.setupPropsDialog() 547 | except Exception as e: 548 | traceback.print_exc() 549 | print e 550 | 551 | def OpenFolder(self, event): 552 | if getattr(self, 'path', None): 553 | os.startfile(self.path) 554 | 555 | def TreePopupRClick(self, event): 556 | id = self.contents_tree.HitTest(event.GetPosition())[0] 557 | self.contents_tree.SelectItem(id, True) # Set right-clicked item as selection for popups 558 | 559 | def TreePopup(self, event): 560 | menu = None 561 | sd = self._selected_data 562 | if sd is self._extensiontoplevel: 563 | menu = self.extensionmenu 564 | elif sd is self._menutoplevel: 565 | menu = self.menumenu 566 | elif sd is self._toolbartoplevel: 567 | menu = self.toolbarmenu 568 | elif isinstance(sd, addin.ControlContainer): 569 | menu = self.controlcontainermenu 570 | elif isinstance(sd, (addin.UIControl, addin.XMLAttrMap)): 571 | menu = wx.Menu() 572 | else: 573 | print sd 574 | if menu: 575 | if sd not in (self._extensiontoplevel, self._menutoplevel, self._toolbartoplevel): 576 | if menu.GetMenuItemCount(): 577 | menu.AppendSeparator() 578 | removecmd = menu.Append(-1, _("Remove")) 579 | def remove(event): 580 | if self.project.addin.remove(sd): 581 | self.contents_tree.Delete(self.contents_tree.GetSelection()) 582 | self.save_button.Enable(True) 583 | menu.Bind(wx.EVT_MENU, remove, removecmd) 584 | self.PopupMenu(menu) 585 | menu.Destroy() 586 | 587 | if __name__ == "__main__": 588 | app = wx.App(redirect=False) 589 | addin_window = AddinMakerAppWindow(None, -1) 590 | app.SetTopWindow(addin_window) 591 | addin_window.Show() 592 | app.MainLoop() 593 | -------------------------------------------------------------------------------- /addin_ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # generated by wxGlade 0.6.3 on Mon Sep 19 12:33:19 2011 4 | 5 | import wx 6 | 7 | # begin wxGlade: extracode 8 | _msize = wx.Size(700, 550) 9 | _msize = wx.Size(700, 550) 10 | # end wxGlade 11 | 12 | 13 | 14 | class AddinMakerWindow(wx.Frame): 15 | def __init__(self, *args, **kwds): 16 | # begin wxGlade: AddinMakerWindow.__init__ 17 | kwds["style"] = wx.DEFAULT_FRAME_STYLE 18 | wx.Frame.__init__(self, *args, **kwds) 19 | self.full_app_panel = wx.Panel(self, -1) 20 | self.bottom_buttons_pane = wx.Panel(self.full_app_panel, -1) 21 | self.propsheet_panel = wx.Panel(self.full_app_panel, -1) 22 | self.tabs_notebook = wx.Notebook(self.propsheet_panel, -1, style=0) 23 | self.project_items_pane = wx.Panel(self.tabs_notebook, -1) 24 | self.item_property_panel = wx.Panel(self.project_items_pane, -1) 25 | self.project_settings_pane = wx.Panel(self.tabs_notebook, -1) 26 | self.logo_panel = wx.Panel(self.propsheet_panel, -1) 27 | self.properties_rows_holder_staticbox = wx.StaticBox(self.project_settings_pane, -1, "Project Properties:") 28 | self.title_panel = wx.Panel(self.full_app_panel, -1) 29 | self.title_label = wx.StaticText(self.title_panel, -1, "Python Add-In Wizard", style=wx.ALIGN_RIGHT) 30 | self.title_divider_line = wx.StaticLine(self.full_app_panel, -1) 31 | self.logo_bitmap = wx.StaticBitmap(self.logo_panel, -1, wx.Bitmap("images\\AddInDesktop64.png", wx.BITMAP_TYPE_ANY)) 32 | self.folder_label = wx.StaticText(self.project_settings_pane, -1, "Working Folder:", style=wx.ALIGN_RIGHT) 33 | self.folder_button = wx.Button(self.project_settings_pane, -1, "Select Folder...") 34 | self.product_label = wx.StaticText(self.project_settings_pane, -1, "Select Product:", style=wx.ALIGN_RIGHT) 35 | self.product_combo_box = wx.ComboBox(self.project_settings_pane, -1, choices=["ArcMap", "ArcGlobe", "ArcScene", "ArcCatalog"], style=wx.CB_DROPDOWN|wx.CB_READONLY) 36 | self.static_line_1 = wx.StaticLine(self.project_settings_pane, -1) 37 | self.project_name_label = wx.StaticText(self.project_settings_pane, -1, "Name*:", style=wx.ALIGN_RIGHT) 38 | self.project_name = wx.TextCtrl(self.project_settings_pane, -1, "") 39 | self.project_version_label = wx.StaticText(self.project_settings_pane, -1, "Version*:", style=wx.ALIGN_RIGHT) 40 | self.project_version = wx.TextCtrl(self.project_settings_pane, -1, "") 41 | self.project_company_label = wx.StaticText(self.project_settings_pane, -1, "Company:", style=wx.ALIGN_RIGHT) 42 | self.project_company = wx.TextCtrl(self.project_settings_pane, -1, "") 43 | self.project_description_label = wx.StaticText(self.project_settings_pane, -1, "Description:", style=wx.ALIGN_RIGHT) 44 | self.project_description = wx.TextCtrl(self.project_settings_pane, -1, "") 45 | self.project_author_label = wx.StaticText(self.project_settings_pane, -1, "Author:", style=wx.ALIGN_RIGHT) 46 | self.project_author = wx.TextCtrl(self.project_settings_pane, -1, "") 47 | self.image_section_divider = wx.StaticLine(self.project_settings_pane, -1) 48 | self.project_image_label = wx.StaticText(self.project_settings_pane, -1, "Image:", style=wx.ALIGN_RIGHT) 49 | self.select_project_image = wx.Button(self.project_settings_pane, -1, "Select Image...") 50 | self.icon_bitmap = wx.StaticBitmap(self.project_settings_pane, -1, wx.Bitmap("images\\AddInDesktop48.png", wx.BITMAP_TYPE_ANY), style=wx.SIMPLE_BORDER) 51 | self.contents_tree = wx.TreeCtrl(self.project_items_pane, -1, style=wx.TR_HAS_BUTTONS|wx.TR_LINES_AT_ROOT|wx.TR_HIDE_ROOT|wx.TR_DEFAULT_STYLE|wx.RAISED_BORDER) 52 | self.bottom_buttons_spacer_panel = wx.Panel(self.bottom_buttons_pane, -1) 53 | self.open_folder = wx.Button(self.bottom_buttons_pane, -1, "Open Folder") 54 | self.save_button = wx.Button(self.bottom_buttons_pane, -1, "Save") 55 | 56 | self.__set_properties() 57 | self.__do_layout() 58 | 59 | self.Bind(wx.EVT_BUTTON, self.SelectFolder, self.folder_button) 60 | self.Bind(wx.EVT_COMBOBOX, self.ComboBox, self.product_combo_box) 61 | self.Bind(wx.EVT_TEXT_ENTER, self.ProjectNameText, self.project_name) 62 | self.Bind(wx.EVT_TEXT, self.ProjectNameText, self.project_name) 63 | self.Bind(wx.EVT_TEXT_ENTER, self.ProjectVersionText, self.project_version) 64 | self.Bind(wx.EVT_TEXT, self.ProjectVersionText, self.project_version) 65 | self.Bind(wx.EVT_TEXT_ENTER, self.ProjectCompanyText, self.project_company) 66 | self.Bind(wx.EVT_TEXT, self.ProjectCompanyText, self.project_company) 67 | self.Bind(wx.EVT_TEXT_ENTER, self.ProjectDescriptionText, self.project_description) 68 | self.Bind(wx.EVT_TEXT, self.ProjectDescriptionText, self.project_description) 69 | self.Bind(wx.EVT_TEXT_ENTER, self.ProjectAuthorText, self.project_author) 70 | self.Bind(wx.EVT_TEXT, self.ProjectAuthorText, self.project_author) 71 | self.Bind(wx.EVT_BUTTON, self.SelectProjectImage, self.select_project_image) 72 | self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.BeginDrag, self.contents_tree) 73 | self.Bind(wx.EVT_TREE_END_DRAG, self.EndDrag, self.contents_tree) 74 | self.Bind(wx.EVT_TREE_DELETE_ITEM, self.DeleteItem, self.contents_tree) 75 | self.Bind(wx.EVT_TREE_SEL_CHANGED, self.SelChanged, self.contents_tree) 76 | self.Bind(wx.EVT_NOTEBOOK_PAGE_CHANGING, self.ChangeTab, self.tabs_notebook) 77 | self.Bind(wx.EVT_BUTTON, self.OpenFolder, self.open_folder) 78 | self.Bind(wx.EVT_BUTTON, self.SaveProject, self.save_button) 79 | # end wxGlade 80 | 81 | def __set_properties(self): 82 | # begin wxGlade: AddinMakerWindow.__set_properties 83 | self.SetTitle("ArcGIS Python Add-In Wizard") 84 | self.SetMinSize(_msize) 85 | self.title_label.SetFont(wx.Font(16, wx.DEFAULT, wx.NORMAL, wx.NORMAL, 0, "")) 86 | self.title_panel.SetBackgroundColour(wx.SystemSettings_GetColour(wx.SYS_COLOUR_3DHIGHLIGHT)) 87 | self.product_combo_box.SetSelection(0) 88 | self.project_name_label.SetMinSize((72, 16)) 89 | self.project_version_label.SetMinSize((72, 16)) 90 | self.project_company_label.SetMinSize((72, 16)) 91 | self.project_description_label.SetMinSize((72, 16)) 92 | self.project_author_label.SetMinSize((72, 16)) 93 | self.project_image_label.SetMinSize((72, 16)) 94 | self.contents_tree.SetMinSize((200, 484)) 95 | # end wxGlade 96 | 97 | def __do_layout(self): 98 | # begin wxGlade: AddinMakerWindow.__do_layout 99 | full_window_sizer = wx.BoxSizer(wx.HORIZONTAL) 100 | main_sizer = wx.BoxSizer(wx.VERTICAL) 101 | bottom_buttons_sizer = wx.BoxSizer(wx.HORIZONTAL) 102 | content_sizer = wx.BoxSizer(wx.HORIZONTAL) 103 | splitter_sizer = wx.BoxSizer(wx.HORIZONTAL) 104 | items_sizer = wx.BoxSizer(wx.HORIZONTAL) 105 | item_property_sizer = wx.BoxSizer(wx.VERTICAL) 106 | fields_sizer = wx.BoxSizer(wx.VERTICAL) 107 | properties_rows_holder = wx.StaticBoxSizer(self.properties_rows_holder_staticbox, wx.VERTICAL) 108 | project_bitmap_display_sizer = wx.BoxSizer(wx.HORIZONTAL) 109 | properties_rows = wx.BoxSizer(wx.VERTICAL) 110 | project_image_sizer = wx.BoxSizer(wx.HORIZONTAL) 111 | project_author_sizer = wx.BoxSizer(wx.HORIZONTAL) 112 | project_description_sizer = wx.BoxSizer(wx.HORIZONTAL) 113 | project_company_sizer = wx.BoxSizer(wx.HORIZONTAL) 114 | project_version_sizer = wx.BoxSizer(wx.HORIZONTAL) 115 | project_name_sizer = wx.BoxSizer(wx.HORIZONTAL) 116 | product_sizer = wx.BoxSizer(wx.HORIZONTAL) 117 | folder_sizer = wx.BoxSizer(wx.HORIZONTAL) 118 | logo_sizer = wx.BoxSizer(wx.HORIZONTAL) 119 | title_sizer = wx.BoxSizer(wx.HORIZONTAL) 120 | title_sizer.Add(self.title_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_HORIZONTAL, 8) 121 | self.title_panel.SetSizer(title_sizer) 122 | main_sizer.Add(self.title_panel, 0, wx.EXPAND|wx.ALIGN_RIGHT, 0) 123 | main_sizer.Add(self.title_divider_line, 0, wx.EXPAND, 0) 124 | logo_sizer.Add(self.logo_bitmap, 0, wx.TOP, 8) 125 | self.logo_panel.SetSizer(logo_sizer) 126 | splitter_sizer.Add(self.logo_panel, 0, wx.EXPAND, 0) 127 | folder_sizer.Add(self.folder_label, 0, wx.ALIGN_CENTER_VERTICAL, 8) 128 | folder_sizer.Add(self.folder_button, 1, wx.EXPAND, 8) 129 | fields_sizer.Add(folder_sizer, 0, wx.ALL|wx.EXPAND, 4) 130 | product_sizer.Add(self.product_label, 0, wx.ALIGN_CENTER_VERTICAL, 3) 131 | product_sizer.Add(self.product_combo_box, 0, 0, 3) 132 | fields_sizer.Add(product_sizer, 0, wx.ALL|wx.EXPAND, 4) 133 | fields_sizer.Add(self.static_line_1, 0, wx.ALL|wx.EXPAND, 8) 134 | project_name_sizer.Add(self.project_name_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 2) 135 | project_name_sizer.Add(self.project_name, 1, wx.EXPAND, 0) 136 | properties_rows.Add(project_name_sizer, 1, wx.EXPAND, 0) 137 | project_version_sizer.Add(self.project_version_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 2) 138 | project_version_sizer.Add(self.project_version, 1, wx.EXPAND, 0) 139 | properties_rows.Add(project_version_sizer, 1, wx.EXPAND, 0) 140 | project_company_sizer.Add(self.project_company_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 2) 141 | project_company_sizer.Add(self.project_company, 1, wx.EXPAND, 0) 142 | properties_rows.Add(project_company_sizer, 1, wx.EXPAND, 0) 143 | project_description_sizer.Add(self.project_description_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 2) 144 | project_description_sizer.Add(self.project_description, 1, wx.EXPAND, 0) 145 | properties_rows.Add(project_description_sizer, 1, wx.EXPAND, 0) 146 | project_author_sizer.Add(self.project_author_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 2) 147 | project_author_sizer.Add(self.project_author, 1, wx.EXPAND, 0) 148 | properties_rows.Add(project_author_sizer, 1, wx.EXPAND, 0) 149 | properties_rows.Add(self.image_section_divider, 0, wx.ALL|wx.EXPAND, 2) 150 | project_image_sizer.Add(self.project_image_label, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 2) 151 | project_image_sizer.Add(self.select_project_image, 0, 0, 0) 152 | properties_rows.Add(project_image_sizer, 1, wx.EXPAND, 3) 153 | properties_rows_holder.Add(properties_rows, 0, wx.EXPAND, 0) 154 | project_bitmap_display_sizer.Add(self.icon_bitmap, 0, wx.TOP, 3) 155 | properties_rows_holder.Add(project_bitmap_display_sizer, 1, wx.LEFT|wx.EXPAND, 80) 156 | fields_sizer.Add(properties_rows_holder, 0, wx.EXPAND, 0) 157 | self.project_settings_pane.SetSizer(fields_sizer) 158 | items_sizer.Add(self.contents_tree, 0, wx.EXPAND, 0) 159 | self.item_property_panel.SetSizer(item_property_sizer) 160 | items_sizer.Add(self.item_property_panel, 1, wx.EXPAND, 0) 161 | self.project_items_pane.SetSizer(items_sizer) 162 | self.tabs_notebook.AddPage(self.project_settings_pane, "Project Settings") 163 | self.tabs_notebook.AddPage(self.project_items_pane, "Add-In Contents") 164 | splitter_sizer.Add(self.tabs_notebook, 1, wx.EXPAND, 4) 165 | content_sizer.Add(splitter_sizer, 1, wx.EXPAND, 4) 166 | self.propsheet_panel.SetSizer(content_sizer) 167 | main_sizer.Add(self.propsheet_panel, 1, wx.EXPAND, 0) 168 | bottom_buttons_sizer.Add(self.bottom_buttons_spacer_panel, 1, wx.EXPAND, 0) 169 | bottom_buttons_sizer.Add(self.open_folder, 0, wx.ALL, 2) 170 | bottom_buttons_sizer.Add(self.save_button, 0, wx.ALL, 2) 171 | self.bottom_buttons_pane.SetSizer(bottom_buttons_sizer) 172 | main_sizer.Add(self.bottom_buttons_pane, 0, wx.EXPAND, 4) 173 | self.full_app_panel.SetSizer(main_sizer) 174 | full_window_sizer.Add(self.full_app_panel, 1, wx.ALL|wx.EXPAND, 1) 175 | self.SetSizer(full_window_sizer) 176 | full_window_sizer.Fit(self) 177 | self.Layout() 178 | self.Centre() 179 | # end wxGlade 180 | 181 | def SelectFolder(self, event): # wxGlade: AddinMakerWindow. 182 | print "Event handler `SelectFolder' not implemented!" 183 | event.Skip() 184 | 185 | def ComboBox(self, event): # wxGlade: AddinMakerWindow. 186 | print "Event handler `ComboBox' not implemented!" 187 | event.Skip() 188 | 189 | def ProjectNameText(self, event): # wxGlade: AddinMakerWindow. 190 | print "Event handler `ProjectNameText' not implemented!" 191 | event.Skip() 192 | 193 | def ProjectVersionText(self, event): # wxGlade: AddinMakerWindow. 194 | print "Event handler `ProjectVersionText' not implemented!" 195 | event.Skip() 196 | 197 | def ProjectCompanyText(self, event): # wxGlade: AddinMakerWindow. 198 | print "Event handler `ProjectCompanyText' not implemented!" 199 | event.Skip() 200 | 201 | def ProjectDescriptionText(self, event): # wxGlade: AddinMakerWindow. 202 | print "Event handler `ProjectDescriptionText' not implemented!" 203 | event.Skip() 204 | 205 | def ProjectAuthorText(self, event): # wxGlade: AddinMakerWindow. 206 | print "Event handler `ProjectAuthorText' not implemented!" 207 | event.Skip() 208 | 209 | def SelectProjectImage(self, event): # wxGlade: AddinMakerWindow. 210 | print "Event handler `SelectProjectImage' not implemented!" 211 | event.Skip() 212 | 213 | def BeginDrag(self, event): # wxGlade: AddinMakerWindow. 214 | print "Event handler `BeginDrag' not implemented!" 215 | event.Skip() 216 | 217 | def EndDrag(self, event): # wxGlade: AddinMakerWindow. 218 | print "Event handler `EndDrag' not implemented!" 219 | event.Skip() 220 | 221 | def DeleteItem(self, event): # wxGlade: AddinMakerWindow. 222 | print "Event handler `DeleteItem' not implemented!" 223 | event.Skip() 224 | 225 | def SelChanged(self, event): # wxGlade: AddinMakerWindow. 226 | print "Event handler `SelChanged' not implemented!" 227 | event.Skip() 228 | 229 | def ChangeTab(self, event): # wxGlade: AddinMakerWindow. 230 | print "Event handler `ChangeTab' not implemented!" 231 | event.Skip() 232 | 233 | def OpenFolder(self, event): # wxGlade: AddinMakerWindow. 234 | print "Event handler `OpenFolder' not implemented!" 235 | event.Skip() 236 | 237 | def SaveProject(self, event): # wxGlade: AddinMakerWindow. 238 | print "Event handler `SaveProject' not implemented!" 239 | event.Skip() 240 | 241 | # end of class AddinMakerWindow 242 | 243 | 244 | if __name__ == "__main__": 245 | app = wx.PySimpleApp(0) 246 | wx.InitAllImageHandlers() 247 | addin_window = AddinMakerWindow(None, -1, "") 248 | app.SetTopWindow(addin_window) 249 | addin_window.Show() 250 | app.MainLoop() 251 | -------------------------------------------------------------------------------- /i18n.py: -------------------------------------------------------------------------------- 1 | __all__ = ['_'] 2 | 3 | import json 4 | import os 5 | 6 | try: 7 | translations_dict = json.loads(open(os.path.join('resources', 'resource_strings.json'), 'rb').read().decode('utf-8')) 8 | except: 9 | print "Can't load translation strings file." 10 | 11 | def _(the_text): 12 | return translations_dict.get(the_text, the_text) 13 | -------------------------------------------------------------------------------- /images/AddInDesktop.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonbot/addin-wizard/eeaf8d8988a0c24d110918241f7bd03a24644c1a/images/AddInDesktop.ico -------------------------------------------------------------------------------- /images/AddInDesktop48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonbot/addin-wizard/eeaf8d8988a0c24d110918241f7bd03a24644c1a/images/AddInDesktop48.png -------------------------------------------------------------------------------- /images/AddInDesktop64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonbot/addin-wizard/eeaf8d8988a0c24d110918241f7bd03a24644c1a/images/AddInDesktop64.png -------------------------------------------------------------------------------- /packaging/README.txt: -------------------------------------------------------------------------------- 1 | This is a stub project created by the ArcGIS Desktop Python AddIn Wizard. 2 | 3 | MANIFEST 4 | ======== 5 | 6 | README.txt : This file 7 | 8 | makeaddin.py : A script that will create a .esriaddin file out of this 9 | project, suitable for sharing or deployment 10 | 11 | config.xml : The AddIn configuration file 12 | 13 | Images/* : all UI images for the project (icons, images for buttons, 14 | etc) 15 | 16 | Install/* : The Python project used for the implementation of the 17 | AddIn. The specific python script to be used as the root 18 | module is specified in config.xml. 19 | 20 | NOTE TO ADD-IN AUTHORS: Please edit this file to be relevant documentation 21 | for your project or delete it before distributing. 22 | -------------------------------------------------------------------------------- /packaging/makeaddin.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import re 4 | import zipfile 5 | 6 | current_path = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | out_zip_name = os.path.join(current_path, 9 | os.path.basename(current_path) + ".esriaddin") 10 | 11 | BACKUP_FILE_PATTERN = re.compile(".*_addin_[0-9]+[.]py$", re.IGNORECASE) 12 | 13 | def looks_like_a_backup(filename): 14 | return bool(BACKUP_FILE_PATTERN.match(filename)) 15 | 16 | with zipfile.ZipFile(out_zip_name, 'w', zipfile.ZIP_DEFLATED) as zip_file: 17 | for filename in ('config.xml', 'README.txt', 'makeaddin.py'): 18 | full_filename = os.path.join(current_path, filename) 19 | if os.path.exists(full_filename): 20 | zip_file.write(full_filename, filename) 21 | dirs_to_add = ['Images', 'Install'] 22 | for directory in dirs_to_add: 23 | for (path, dirs, files) in os.walk(os.path.join(current_path, 24 | directory)): 25 | archive_path = os.path.relpath(path, current_path) 26 | found_file = False 27 | for file in (f for f in files if not looks_like_a_backup(f)): 28 | archive_file = os.path.join(archive_path, file) 29 | print archive_file 30 | zip_file.write(os.path.join(path, file), archive_file) 31 | found_file = True 32 | if not found_file: 33 | zip_file.writestr(os.path.join(archive_path, 34 | 'placeholder.txt'), 35 | "(Empty directory)") 36 | -------------------------------------------------------------------------------- /resources/resource_strings.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "**** THIS STRING RESOURCE FILE IS EXPECTED TO BE ENCODED IN UTF-8 ***": "(Do not translate this string)", 4 | "It is arranged into ORIGINAL: TRANSLATION pairs.": "(Do not translate this string)", 5 | 6 | "Add-In Contents": "Add-In Contents", 7 | "ArcGIS Python Add-In Wizard": "ArcGIS Python Add-In Wizard", 8 | "Author:": "Author:", 9 | "Button": "Button", 10 | "Caption": "Caption", 11 | "Choose a directory to use as an Add-In project root:": "Choose a directory to use as an Add-In project root:", 12 | "Class Name": "Class Name", 13 | "Column Count": "Column Count", 14 | "Company:": "Company:", 15 | "Description": "Description", 16 | "Description:": "Description:", 17 | "EXTENSIONS": "EXTENSIONS", 18 | "Extension": "Extension", 19 | "Extensions": "Extensions", 20 | "Has Separator": "Has Separator", 21 | "Help Content": "Help Content", 22 | "Help Heading": "Help Heading", 23 | "Hint Text": "Hint Text", 24 | "ID (Variable Name)": "ID (Variable Name)", 25 | "Image for Control": "Image for Control", 26 | "Image for control:": "Image for control:", 27 | "Image:": "Image:", 28 | "Is Shortcut Menu": "Is Shortcut Menu", 29 | "Load Automatically": "Load Automatically", 30 | "MENUS": "MENUS", 31 | "Menu": "Menu", 32 | "Menu Style": "Menu Style", 33 | "Message": "Message", 34 | "Methods to Implement": "Methods to Implement", 35 | "Name": "Name", 36 | "Name*:": "Name*:", 37 | "New Extension": "New Extension", 38 | "New Menu": "New Menu", 39 | "Open Folder": "Open Folder", 40 | "Project Properties:": "Project Properties:", 41 | "Project Settings": "Project Settings", 42 | "Python Add-In Wizard": "Python Add-In Wizard", 43 | "Save": "Save", 44 | "Save before exiting?": "Save before exiting?", 45 | "Save your changes before exiting?": "Save your changes before exiting?", 46 | "Select Folder...": "Select Folder...", 47 | "Select Image...": "Select Image...", 48 | "Select Product:": "Select Product:", 49 | "Show Initially": "Show Initially", 50 | "TOOLBARS": "TOOLBARS", 51 | "Toolbar": "Toolbar", 52 | "Toolbars": "Toolbars", 53 | "Tooltip": "Tooltip", 54 | "Toplevel Menus": "Toplevel Menus", 55 | "Version*:": "Version*:", 56 | "Working Folder:": "Working Folder:" 57 | } 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import py2exe 3 | import glob 4 | 5 | setup(windows=[ 6 | {'script': 'addin_assistant.pyw', 7 | 'icon_resources': [(1, "images\\AddInDesktop.ico")] 8 | } 9 | ], 10 | options={ "py2exe": { "dll_excludes": ["MSVCP90.dll"] }}, 11 | data_files=[('images', glob.glob("images\\*.png") + 12 | glob.glob("images\\*.ico")), 13 | ('packaging', glob.glob("packaging\\*.*")), 14 | ('resources', glob.glob("resources\\*.*"))] 15 | ) 16 | --------------------------------------------------------------------------------