1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
42 """
43 Base class for anything that wants to be notified about state changes
44 in the map
45 """
46
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
56 """
57 @rtype: MapController
58 @return: the controller that this is a listener for
59 """
60 return self.__controller
61
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
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
88 """
89 @type layerName: string
90 @param layerName: name of the new layer
91 """
92 pass
93
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
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
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
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
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
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
153 """
154 Called when the map being edited is closed. Used to make buttons
155 insensitive, free resources, etc.
156 """
157 pass
158
160 """
161 Called when a map is opened or created. Used to make buttons sensitive,
162 or whatever else.
163 """
164 pass
165
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
184 """
185 @type fileName: string
186 @param fileName: name of the file that was
187 """
188 pass
189
191 """
192 Update due to an undo action or a redo action
193 """
194 pass
195
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
205 """
206 Notify the listener that a shape was removed
207 @type shape: shapes.Shape
208 @param shape: the shape added
209 """
210
211
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 """
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
229 self.__thumbnailSource = source
230
232 if self.__thumbnailSource != None:
233 return self.__thumbnailSource.getThumbnail(largest)
234 else:
235 return None
236
239
243
245 """
246 @rtype: graphics.RGBA
247 @return: Map's background color
248 """
249 return self.__map.getBGColor()
250
252 """
253 @type color: graphics.RGBA
254 @param color: the new color
255 """
256 self.__map.setBGColor(color)
257 self.notifyModification(True)
258
261
264
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
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
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
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
323
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
355
357 assert(self.hasMap())
358 return self.__map.tileSize
359
361 return self.__map.width
362
364 return self.__map.height
365
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
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
385 """
386 @rtype: int
387 @return: the currently selected layer
388 """
389 return self.__selectedLayer
390
392 """
393 @rtype: bool
394 @return: True if the layer at index is visible
395 """
396 return self.__map.layers[index].visible
397
403
407
409 """
410 @rtype: string
411 @return: the name of the file currently loaded
412 """
413 return self.__fileName
414
416 self.__fileName = fileName
417
419 """
420 Brief Description
421 @type listener: MapListener
422 @param listener: Placeholder
423 """
424 self.__listeners.append(listener)
425
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
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
461
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
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
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
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
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
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
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
616
617
618 pass
619 return layers
620
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
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
646 self.__toplevel = widget
647
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
660
666
672