Package arcmap :: Module mapcontroller
[hide private]
[frames] | no frames]

Source Code for Module arcmap.mapcontroller

  1  ################################################################################ 
  2  # Authors: Brian Schott (Sir Alaran) 
  3  # Copyright: Brian Schott (Sir Alaran) 
  4  # Date: Sep 22 2009 
  5  # License: 
  6  # 
  7  # This program is free software: you can redistribute it and/or modify 
  8  # it under the terms of the GNU General Public License as published by 
  9  # the Free Software Foundation, either version 3 of the License, or 
 10  # (at your option) any later version. 
 11  # 
 12  # This program is distributed in the hope that it will be useful, 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
 15  # GNU General Public License for more details. 
 16  # 
 17  # You should have received a copy of the GNU General Public License 
 18  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 19  ################################################################################ 
 20   
 21  """ 
 22  Contains the MapController and MapListener classes. 
 23  """ 
 24   
 25  __docformat__ = "epytext" 
 26   
 27  import os 
 28  import logging 
 29  import copy 
 30   
 31  import gtk 
 32  import cairo 
 33   
 34  import tilemap 
 35  import graphics 
 36  import undo 
 37  import datafiles 
 38   
 39  log = logging.getLogger("mapcontroller") 
 40   
41 -class MapListener:
42 """ 43 Base class for anything that wants to be notified about state changes 44 in the map 45 """ 46
47 - def __init__(self, controller):
48 """ 49 @type controller: MapController 50 @param controller: The controller that this will listen to 51 """ 52 self.__controller = controller 53 self.__controller.addListener(self)
54
55 - def getController(self):
56 """ 57 @rtype: MapController 58 @return: the controller that this is a listener for 59 """ 60 return self.__controller
61
62 - def listenSetVisibilty(self, index, visible):
63 """ 64 @type index: int 65 @param index: the index of the layer to set visible/invisible 66 @type visible: bool 67 @param visible: True if the layer should be visible, False otherwise 68 """ 69 pass
70
71 - def listenResize(self, width, height, xoffset, yoffset):
72 """ 73 Brief Description 74 @type width: int 75 @param width: The new width in tiles 76 @type height: int 77 @param height: The new height in tiles 78 @type xoffset: int 79 @param xoffset: The number of tiles that the existing map contents 80 should be shifted right 81 @type yoffset: int 82 @param yoffset: The number of tiles that the existing map contents 83 should be shifted down 84 """ 85 pass
86
87 - def listenAddLayer(self, layerName):
88 """ 89 @type layerName: string 90 @param layerName: name of the new layer 91 """ 92 pass
93
94 - def listenAddTile(self, source, x, y, z):
95 """ 96 @type source: cairo.ImageSurface 97 @param source: source surface for drawing 98 @type x: int 99 @param x: x-coordinate in tiles 100 @type y: int 101 @param y: y-coordinate in tiles 102 @type z: int 103 @param z: layer index of the tile to delete 104 """ 105 pass
106
107 - def listenRemoveTile(self, x, y, z):
108 """ 109 @type x: int 110 @param x: x-coordinate of tile to delete 111 @type y: int 112 @param y: y-coordinate of tile to delete 113 @type z: int 114 @param z: layer index of the tile to delete 115 """ 116 pass
117
118 - def listenRemoveLayer(self, index):
119 """ 120 Notify the listener that a layer has been removed 121 @type index: int 122 @param index: the index of the removed layer 123 """ 124 pass
125
126 - def listenSwapLayers(self, index1, index2):
127 """ 128 Notify listener that two layers have changed places 129 @type index1: int 130 @param index1: index of first layer 131 @type index2: int 132 @param index2: index of second layer 133 """ 134 pass
135
136 - def listenSelectLayer(self, index):
137 """ 138 Update the listener to a change in layer selection 139 @type index: int 140 @param index: index of the layer that was selected 141 """ 142 pass
143
144 - def listenModified(self, modified):
145 """ 146 Notify the listener if the map has unsaved changes or not 147 @type modified: bool 148 @param modified: Whether or not the map is changed 149 """ 150 pass
151
152 - def listenFileClosed(self):
153 """ 154 Called when the map being edited is closed. Used to make buttons 155 insensitive, free resources, etc. 156 """ 157 pass
158
159 - def listenFileOpened(self):
160 """ 161 Called when a map is opened or created. Used to make buttons sensitive, 162 or whatever else. 163 """ 164 pass
165
166 - def listenSetSelection(self, index, brush, x1, y1, x2, y2):
167 """ 168 @type index: int 169 @param index: index of the image 170 @type brush: cairo.ImageSurface 171 @param brush: pixel data to use 172 @type x1: int 173 @param x1: left coordinate of selection in tiles 174 @type y1: int 175 @param y1: top coordinate of selection in tiles 176 @type x2: int 177 @param x2: right coordinate of selection in tiles 178 @type y2: int 179 @param y2: bottom coordinate of selection in tiles 180 """ 181 pass
182
183 - def listenAddTileSet(self, fileName):
184 """ 185 @type fileName: string 186 @param fileName: name of the file that was 187 """ 188 pass
189
190 - def listenUndoRedo(self):
191 """ 192 Update due to an undo action or a redo action 193 """ 194 pass
195
196 - def listenAddShape(self, shape):
197 """ 198 Notify the listener that a shape was added 199 @type shape: shapes.Shape 200 @param shape: the shape added 201 """ 202 pass
203
204 - def listenRemoveShape(self, shape):
205 """ 206 Notify the listener that a shape was removed 207 @type shape: shapes.Shape 208 @param shape: the shape added 209 """
210 211
212 -class MapController:
213 """ 214 MapController is the command and control center for the program. It manages 215 communication between the other modules and controls the tilemap. 216 """
217 - def __init__(self):
218 219 self.__listeners = [] 220 self.__images = [] 221 self.__fileName = None 222 self.__modified = False 223 self.__selectedLayer = None 224 self.__map = None 225 self.__thumbnailSource = None 226 self.__toplevel = None
227
228 - def setThumbnailSource(self, source):
229 self.__thumbnailSource = source
230
231 - def getThumbnail(self, largest):
232 if self.__thumbnailSource != None: 233 return self.__thumbnailSource.getThumbnail(largest) 234 else: 235 return None
236
237 - def getParallaxes(self):
238 return self.__map.getParallaxes()
239
240 - def setParallaxes(self, parallaxes):
241 self.__map.setParallaxes(parallaxes) 242 self.notifyModification(True)
243
244 - def getBGColor(self):
245 """ 246 @rtype: graphics.RGBA 247 @return: Map's background color 248 """ 249 return self.__map.getBGColor()
250
251 - def setBGColor(self, color):
252 """ 253 @type color: graphics.RGBA 254 @param color: the new color 255 """ 256 self.__map.setBGColor(color) 257 self.notifyModification(True)
258
259 - def getShapes(self):
260 return self.__map.getShapes()
261
262 - def setShapes(self, shapes):
263 self.__map.setShapes(shapes)
264
265 - def addShape(self, shape):
266 """ 267 @type shape: shapes.Shape 268 @param shape: the shape to add to the map 269 """ 270 self.__map.addShape(shape) 271 for listener in self.__listeners: 272 listener.listenAddShape(shape) 273 self.notifyModification(True)
274
275 - def removeShape(self, shape):
276 """ 277 @type shape: shapes.Shape 278 @param shape: the shape to remove 279 """ 280 self.__map.delShape(shape) 281 for listener in self.__listeners: 282 listener.listenRemoveShape(shape) 283 self.notifyModification(True)
284
285 - def hasMap(self):
286 """ 287 @rtype: bool 288 @return: True if there is a map to edit 289 """ 290 return (self.__map != None)
291
292 - def open(self, fileName):
293 """ 294 Opens a file 295 @type fileName: str 296 @param fileName: the path of the file to open 297 @rtype: str 298 @return: Returns a string saying why the file could not be opened, or 299 None 300 """ 301 # Only one map can be used at a time 302 self.close() 303 self.__fileName = fileName 304 if fileName != None and os.path.exists(fileName): 305 try: 306 self.__map = tilemap.TileMap.readFromFile(fileName) 307 except datafiles.FileNotFoundError as e: 308 self.__fileName = None 309 return str(e) 310 self.__selectedLayer = len(self.__map.layers) - 1 311 for index, fileName in enumerate(self.__map.images): 312 for listener in self.__listeners: 313 listener.listenAddTileSet(fileName) 314 for listener in self.__listeners: 315 listener.listenFileOpened() 316 listener.listenSelectLayer(self.__selectedLayer) 317 return None 318 else: 319 self.__fileName = None 320 return "Could not find the file \"%s\"" % fileName
321
322 - def close(self):
323 # Ignore this call if there is no map currently loaded 324 if self.__map == None: 325 return 326 self.__map = None 327 self.__images = [] 328 self.__modified = False 329 self.__selectedLayer = None 330 self.__fileName = None 331 self.__selectedLayer = None 332 for listener in self.__listeners: 333 listener.listenFileClosed()
334
335 - def new(self, tileSize, width, height):
336 """ 337 Creates a new map of the given width, height, and tile dimentions 338 @type tileSize: int 339 @param tileSize: size of a tile in pixels 340 @type width: int 341 @param width: width of map in tiles 342 @type height: int 343 @param height: height of map in tiles 344 """ 345 self.close() 346 self.__map = tilemap.TileMap.createMap(tileSize, width, height) 347 self.__selectedLayer = len(self.__map.layers) - 1 348 for listener in self.__listeners: 349 listener.listenFileOpened() 350 listener.listenSelectLayer(self.__selectedLayer)
351
352 - def save(self):
353 self.__map.writeToFile(self.__fileName) 354 self.notifyModification(False)
355
356 - def mapTileSize(self):
357 assert(self.hasMap()) 358 return self.__map.tileSize
359
360 - def mapWidth(self):
361 return self.__map.width
362
363 - def mapHeight(self):
364 return self.__map.height
365
366 - def mapLayers(self):
367 return len(self.__map.layers)
368
369 - def resize(self, width, height, xOffset, yOffset):
370 self.__map.resize(width, height, xOffset, yOffset) 371 for listener in self.__listeners: 372 listener.listenResize(width, height, xOffset, yOffset) 373 self.notifyModification(True)
374
375 - def selectLayer(self, index):
376 """ 377 @type index: int 378 @param index: index of layer to select 379 """ 380 self.__selectedLayer = index 381 for listener in self.__listeners: 382 listener.listenSelectLayer(index)
383
384 - def selectedLayer(self):
385 """ 386 @rtype: int 387 @return: the currently selected layer 388 """ 389 return self.__selectedLayer
390
391 - def mapLayerVisibility(self, index):
392 """ 393 @rtype: bool 394 @return: True if the layer at index is visible 395 """ 396 return self.__map.layers[index].visible
397
398 - def setMapLayerVisibility(self, index, v):
399 self.__map.layers[index].visible = v 400 for listener in self.__listeners: 401 listener.listenSetVisibilty(index, v) 402 self.notifyModification(True)
403
404 - def setLayerName(self, index, name):
405 self.__map.layers[index].name = name 406 self.notifyModification(True)
407
408 - def getFileName(self):
409 """ 410 @rtype: string 411 @return: the name of the file currently loaded 412 """ 413 return self.__fileName
414
415 - def setFileName(self, fileName):
416 self.__fileName = fileName
417
418 - def addListener(self, listener):
419 """ 420 Brief Description 421 @type listener: MapListener 422 @param listener: Placeholder 423 """ 424 self.__listeners.append(listener)
425
426 - def openTileSet(self, fileName):
427 """ 428 Opens an image file and adds it to the list of open images 429 @type fileName: string 430 @param fileName: the name of the file to open 431 @rtype: (cairo.ImageSurface, int) 432 @return: the opened image and its index 433 """ 434 image = graphics.loadImage(fileName) 435 if image != None: 436 self.__images.append(image) 437 index = len(self.__images) - 1 438 self.__map.addImage(fileName, index) 439 return image, index 440 else: 441 return None, 0
442
443 - def drawShapes(self, context):
444 """ 445 Draws the shapes in the map to the given context 446 @type context: cairo.Context 447 @param context: context to draw on 448 """ 449 for shape in self.__map.shapes: 450 graphics.drawShape(shape, context)
451
452 - def addLayer(self, layerName, visible):
453 """ 454 @type layerName: string 455 @param layerName: name for the new layer 456 @type visible: bool 457 @param visible: True if the layer is visible 458 """ 459 self.__map.addLayer(layerName, True) 460 # select the new layer. This also ensures that a layer is selected on 461 # map load. 462 self.__selectedLayer = len(self.__map.layers) - 1 463 for listener in self.__listeners: 464 listener.listenAddLayer(layerName) 465 listener.listenSelectLayer(len(self.__map.layers)) 466 self.notifyModification(True)
467
468 - def removeLayer(self, index):
469 """ 470 @type index: int 471 @param index: the index of the layer to remove 472 """ 473 self.__map.removeLayer(index) 474 for listener in self.__listeners: 475 listener.listenRemoveLayer(index) 476 self.notifyModification(True)
477
478 - def swapLayers(self, firstIndex, secondIndex):
479 """ 480 @type firstIndex: int 481 @param firstIndex: first layer index 482 @type secondIndex: int 483 @param secondIndex: second layer index 484 """ 485 self.__map.swapLayers(firstIndex, secondIndex) 486 for listener in self.__listeners: 487 listener.listenSwapLayers(firstIndex, secondIndex) 488 self.notifyModification(True)
489
490 - def setSelection(self, index, x1, y1, x2, y2):
491 """ 492 Sets the tile selection. x1 must be less than x2. y1 must be less than 493 y2. 494 @type index: int 495 @param index: index of tile set 496 @type x1: int 497 @param x1: left edge, in tiles 498 @type y1: int 499 @param y1: top edge, in tiles 500 @type x2: int 501 @param x2: right edge, in tiles 502 @type y2: int 503 @param y2: bottom edge, in tiles 504 """ 505 ts = self.__map.tileSize 506 brushSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 507 int((abs(x2 - x1) + 1) * ts), int((abs(y2 - y1) + 1) * ts)) 508 context = cairo.Context(brushSurface) 509 context.set_source_surface(self.__images[index], -(x1 * ts), -(y1 * ts)) 510 context.set_operator(cairo.OPERATOR_SOURCE) 511 context.paint() 512 for listener in self.__listeners: 513 listener.listenSetSelection(index, brushSurface, x1, y1, x2, y2)
514
515 - def addTile(self, x, y, z, ix, iy, ii):
516 """ 517 Adds a tile to the currently selected layer. 518 @type x: int 519 @param x: x-coordinate of tile 520 @type y: int 521 @param y: y-coordinate of tile 522 @type z: int 523 @param z: layer of the tile 524 @type ix: int 525 @param ix: x-coordinate of the image for the tile 526 @type iy: int 527 @param iy: y-coordinate of the image for the tile 528 @type ii: int 529 @param ii: image index 530 @rtype: (int, int, int) 531 @return: The x-coordinate, y-coordinate, and index of the image of the 532 tile that used to be at the coordinates (x, y, z), or None if there 533 was no tile there before 534 """ 535 t = tilemap.Tile(ii, ix, iy) 536 ts = self.__map.tileSize 537 r = self.__map.addTile(t, x, y, z) 538 tileSurface = cairo.ImageSurface(cairo.FORMAT_ARGB32, ts, ts) 539 context = cairo.Context(tileSurface) 540 context.set_source_surface(self.__images[ii], -(ix * ts), -(iy * ts)) 541 context.paint() 542 for listener in self.__listeners: 543 listener.listenAddTile(tileSurface, x, y, z) 544 self.notifyModification(True) 545 return r
546
547 - def removeTile(self, x, y, z):
548 """ 549 Removes a tile from the selected layer 550 @type x: int 551 @param x: the x-coordinate of the tile to remove 552 @type y: int 553 @param y: the y-coordinate of the tile to remove 554 @rtype: (int, int, int) 555 @return: The x-coordinate, y-coordinate, and index of the image of the 556 tile that used to be at the coordinates (x, y, z), or None if there 557 was no tile there before 558 """ 559 if z == -1: 560 z = self.__selectedLayer 561 r = self.__map.removeTile(x, y, z) 562 for listener in self.__listeners: 563 listener.listenRemoveTile(x, y, z) 564 self.notifyModification(True) 565 return r
566
567 - def getLayerInfo(self):
568 """ 569 @rtype: [(string, bool)] 570 @return: names of layers and whether or not they are visible 571 """ 572 info = [] 573 for l in self.__map.layers: 574 info.append((l.name, l.visible)) 575 return info
576
577 - def drawTile(self, tile, context, x, y):
578 """ 579 @type tile: Tile 580 @param tile: tile to draw 581 @type context: cairo.Context 582 @param context: context to draw on 583 @type x: int 584 @param x: x-coordinate of tile 585 @type y: int 586 @param y: y-coordinate of tile 587 """ 588 ts = self.__map.tileSize 589 ix, iy, index = tile.getImageInfo() 590 context.set_source_surface(self.__images[index], (x - ix) * ts, 591 (y - iy) * ts) 592 context.set_operator(cairo.OPERATOR_SOURCE) 593 context.rectangle(x * ts, y * ts, ts, ts) 594 context.fill()
595
596 - def drawLayers(self):
597 """ 598 Draws layers from tile data 599 @rtype: cairo.ImageSurface[] 600 @return: the layers of the map as 32-bit bitmaps 601 """ 602 layers = [] 603 for layer in self.__map.layers: 604 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 605 self.__map.width * self.__map.tileSize, 606 self.__map.height * self.__map.tileSize) 607 context = cairo.Context(surface) 608 try: 609 for x in range(self.__map.width): 610 for y in range(self.__map.height): 611 if (x, y) in layer.tiles: 612 self.drawTile(layer.tiles[(x, y)], context, x, y) 613 layers.append(surface) 614 except IndexError: 615 # Fail silently at this point. An index error probably means that 616 # a tile image could not be opened. The user has already been 617 # warned about this. 618 pass 619 return layers
620
621 - def unsaved(self):
622 """ 623 @rtype: bool 624 @return: true if the map has unsaved changes, false otherwise 625 """ 626 if self.__map == None: 627 return False 628 elif self.__modified == False: 629 return False 630 else: 631 return True
632
633 - def notifyModification(self, modified):
634 """ 635 Notify the controller (and all of its listeners) of a modification to 636 the map. 637 @type modified: bool 638 @param modified: True if the map has been modified, False if it no 639 longer has unsaved changes 640 """ 641 self.__modified = modified 642 for listener in self.__listeners: 643 listener.listenModified(self.__modified)
644
645 - def setToplevel(self, widget):
646 self.__toplevel = widget
647
648 - def getToplevel(self):
649 """ 650 This function is here so that dialogs can get a reference to the main 651 window to calculate their position. 652 """ 653 return self.__toplevel
654
655 - def undo(self):
656 undo.undo() 657 self.notifyModification(undo.canUndo()) 658 for listener in self.__listeners: 659 listener.listenUndoRedo()
660
661 - def redo(self):
662 undo.redo() 663 self.notifyModification(True) 664 for listener in self.__listeners: 665 listener.listenUndoRedo()
666
667 - def addUndoAction(self, action):
668 undo.addUndoAction(action) 669 self.notifyModification(True) 670 for listener in self.__listeners: 671 listener.listenUndoRedo()
672