1#!/usr/local/bin/python3.8 2''' 3trackplacer -- map journey track editor. 4 5usage: trackplacer [-vh?] [filename] 6 7A journey is an object containing a map file name and a (possibly 8empty) list of tracks, each with a name and each consisting of a 9sequence of track markers. This program exists to visually edit 10journeys represented as specially delimited sections in .cfg files. 11 12If the .cfg filename is not specified, trackplacer will enter a loop 13in which it repeatedly pops up a file selector. Canceling the file 14select ends the program; Selecting a file takes you to a main screen. 15For command help on the main screen, click the Help button. 16 17Can be started with a map image, in which case we are editing a new journey. 18Can be started with a .cfg file, in which case it will look for 19track information enclosed in special comments that look like this: 20 21 # trackplacer: tracks begin 22 # trackplacer: tracks end 23 24trackplacer will alter only what it finds inside these comments, except tht it 25will also generate a file epilog for undefining local symbols. The 26epilog will begin with this comment: 27 28 # trackplacer: epilog begins 29 30Special comments may appear in the track section, looking like this: 31 32 # trackplacer: <property>=<value> 33 34These set properties that trackplacer may use. At present there is 35only one such property: "map", which records the name of the mapfile on 36which your track is laid. 37 38Normally, trackplacer assumes it is running within a Battle for 39Wesnoth source tree and changes directory to the root of the 40tree. Paths saved in track files are relative to the tree root. All 41pathnames in help and error messages are also relativized to that 42root. 43 44The -v option enables verbose logging to standard error. 45 46The -d option sets the root directory to use. 47 48The -h or -? options display this summary. 49 50For details on the editing controls, click the Help button in the trackplacer 51GUI. 52''' 53 54gui_help = '''\ 55You are editing or creating a set of named tracks; at any given time there will one track that is selected for editing. For campaigns with a linear narrative there will be only one track, always selected, and you will not have to concern yourself about its name. If your campaign has a non-linear structure, you will want to create one track for each segment. 56 57The radio buttons near the top left corner control which icon is placed by a left click. The two rightmost are special; when the trashcan is clicked a left click deletes already-placed icons, and the convert/copy icon tries to copy a nearby icon from an unselected track onto the selected one, preserving its pixel coordinates exactly. Every time you place an icon, it is added to the currently selected track. You may also drag icons with the middle button. 58 59The rule for adding markers to the selected track is as follows: if the two markers closest to the mouse pointer are adjacent on the track, insert the new marker between them in the track order. Otherwise, append it to the end of the track. 60 61Click the right button to examine features overlapping the pointer. Each marker on both selected and unselected tracks will be reported. 62 63The Animate button clears the icons off the map and places them with a delay after each placement, so you can see what order they are drawn in. If you have multiple tracks, only those currently visible will be animated. 64 65The Save button pops up a file selector asking you to supply a filename to which the track should be saved in .cfg format, as a series of macros suitable for inclusion in WML. Any other extension than .cfg on the filename will raise an error. 66 67The Properties button pops up a list of track properties - key/value pairs associated with the track. All tracks have the property "map" with their associated map name as the value. 68 69The Tracks button pops up a list of controls, one for each track. You can change the state of the checkboxes to control which tracks are visible. The radiobuttons can be used to select a track for editing. You can also add and rename tracks here. Hover over the controls for tooltips. 70 71The Help button displays this message. 72 73The Quit button ends your session, asking for confirmation if you have unsaved changes. 74''' 75 76gui_about = '''\ 77This is trackplacer, an editor for visually editing sets of journey tracks on Battle For Wesnoth maps. 78 79By Eric S. Raymond for the Battle For Wesnoth project, October 2008 80''' 81 82 83import sys, os, re, math, time, exceptions, getopt 84 85import pygtk 86pygtk.require('2.0') 87import gtk 88 89import wesnoth.wmltools 90 91# All dependencies on the shape of the data tree live here 92# The code does no semantic interpretation of these icons at all; 93# to add new ones, just fill in a dictionary entry. 94imagedir = "data/core/images/" 95default_map = imagedir + "maps/wesnoth.png" 96selected_icon_dictionary = { 97 "JOURNEY": imagedir + "misc/new-journey.png", 98 "BATTLE": imagedir + "misc/new-battle.png", 99 "REST": imagedir + "misc/flag-red.png", 100 } 101unselected_icon_dictionary = { 102 "JOURNEY": imagedir + "misc/dot-white.png", 103 "BATTLE": imagedir + "misc/cross-white.png", 104 "REST": imagedir + "misc/flag-white.png", 105 } 106icon_presentation_order = ("JOURNEY", "BATTLE", "REST") 107segmenters = ("BATTLE","REST") 108 109class IOException(exceptions.Exception): 110 "Exception thrown while reading a track file." 111 def __init__(self, message, path, lineno=None): 112 self.message = message 113 self.path = path 114 self.lineno = lineno 115 116# Basic functions for bashing points and rectangles 117 118def distance(x1, y1, x2, y2): 119 "Euclidean distance." 120 return math.sqrt((x1 - x2)**2 + abs(y1 - y2)**2) 121 122def within(x, y, (l, t, r, d)): 123 "Is point within specified rectangle?" 124 if x >= l and x <= l + r - 1 and y >= t and y <= t + d - 1: 125 return True 126 return False 127 128def overlaps(p1, p2): 129 "Do two rectangles overlap?" 130 (x1,y1,x1d,y1d) = p1 131 (x2,y2,x2d,y2d) = p2 132 return within(x1, y1, p2) or \ 133 within(x1+x1d, y1, p2) or \ 134 within(x1, y1+y1d, p2) or \ 135 within(x1+x1d, y1+y1d, p2) or \ 136 within(x2, y2, p1) or \ 137 within(x2+x2d, y2, p1) or \ 138 within(x2, y2+y2d, p1) or \ 139 within(x2+x2d, y2+y2d, p1) 140 141class JourneyTracks: 142 "Represent a set of named journey tracks on a map." 143 def __init__(self): 144 self.mapfile = None # Map background of the journey 145 self.tracks = {} # Dict of lists of (action, x, y) tuples 146 self.selected_id = None 147 self.modifications = 0 148 self.track_order = [] 149 self.properties = {} 150 self.modified = 0 151 self.before = self.after = "" 152 def selected_track(self): 153 "Select a track for modification" 154 return self.tracks[self.selected_id] 155 def set_selected_track(self, name): 156 self.selected_id = name 157 def write(self, filename): 158 "Record a set of named journey tracks." 159 if filename.endswith(".cfg"): 160 fp = open(filename, "w") 161 fp.write(self.before) 162 fp.write("# trackplacer: tracks begin\n#\n") 163 fp.write("# Hand-hack this section strictly at your own risk.\n") 164 fp.write("#\n") 165 if not self.before and not self.after: 166 fp.write("#\n# wmllint: no translatables\n\n") 167 for (key, val) in self.properties.items(): 168 fp.write("# trackplacer: %s=%s\n" % (key, val)) 169 fp.write("#\n") 170 definitions = [] 171 for name in self.track_order: 172 track = self.tracks[name] 173 index_tuples = zip(xrange(len(track)), track) 174 index_tuples = filter(lambda (i, (a, x, y)): a in segmenters, 175 index_tuples) 176 endpoints = map(lambda (i, t): i, index_tuples) 177 if track[-1][0] not in segmenters: 178 endpoints.append(len(track)-1) 179 outname = name.replace(" ", "_").upper() 180 for (i, e) in enumerate(endpoints): 181 stagename = "%s_STAGE%d" % (outname, i+1,) 182 definitions.append(stagename) 183 fp.write("#define %s\n" % stagename) 184 for j in xrange(0, e+1): 185 age="OLD" 186 if i == 0 or j > endpoints[i-1]: 187 age = "NEW" 188 waypoint = (age,) + tuple(track[j]) 189 marker = " {%s_%s %d %d}\n" % waypoint 190 fp.write(marker) 191 fp.write("#enddef\n\n") 192 endname = "%s_END" % stagename 193 fp.write("#define %s\n" % endname) 194 definitions.append(endname) 195 for j in xrange(0, e+1): 196 age="OLD" 197 if j == endpoints[i]: 198 age = "NEW" 199 waypoint = (age,) + tuple(track[j]) 200 marker = " {%s_%s %d %d}\n" % waypoint 201 fp.write(marker) 202 fp.write("#enddef\n\n") 203 completename = "%s_COMPLETE" % name 204 fp.write("#define %s\n" % completename) 205 definitions.append(completename) 206 for j in xrange(len(track)): 207 waypoint = track[j] 208 fp.write(" {OLD_%s %d %d}\n" % tuple(waypoint)) 209 fp.write("#enddef\n\n") 210 fp.write("# trackplacer: tracks end\n") 211 fp.write(self.after) 212 fp.write ("# trackplacer: epilog begins\n\n") 213 for name in definitions: 214 if "{" + name + "}" not in self.after: 215 fp.write("#undef %s\n" % name) 216 fp.write ("\n# trackplacer: epilog ends\n") 217 fp.close() 218 self.modified = 0 219 else: 220 raise IOException("File must have .cfg extension.", fp.name) 221 def read(self, fp): 222 "Initialize a journey from map and track information." 223 if type(fp) == type(""): 224 try: 225 fp = open(fp, "rU") 226 except IOError: 227 raise IOException("Cannot read file.", fp) 228 if self.tracks: 229 raise IOException("Reading with tracks nonempty.", fp.name) 230 if fp.name.endswith(".png") or fp.name.endswith(".jpg"): 231 self.mapfile = self.properties['map'] = fp.name 232 self.selected_id = "JOURNEY" 233 self.add_track(self.selected_id) 234 self.modified = 0 235 return 236 if not fp.name.endswith(".cfg"): 237 raise IOException("Cannot read this filetype.", fp.name) 238 waypoint_re = re.compile("{NEW_(" + "|".join(icon_presentation_order) + ")" \ 239 + " +([0-9]+) +([0-9]+)}") 240 property_re = re.compile("# *trackplacer: ([^=]+)=(.*)") 241 define_re = re.compile("#define (.*)_STAGE[0-9]+(_END|_COMPLETE)?") 242 state = "before" 243 ignore = False 244 for line in fp: 245 if line.startswith("# trackplacer: epilog begins"): 246 break 247 # This is how we ignore stuff outside of track sections 248 if state == "before": 249 if line.startswith("# trackplacer: tracks begin"): 250 state = "tracks" # And fall through... 251 else: 252 self.before += line 253 continue 254 elif state == "after": 255 self.after += line 256 continue 257 elif line.startswith("# trackplacer: tracks end"): 258 state = "after" 259 continue 260 # Which track are we appending to? 261 m = re.search(define_re, line) 262 if m: 263 self.selected_id = m.group(1) 264 ignore = m.group(2) 265 if self.selected_id not in self.track_order: 266 self.track_order.append(self.selected_id) 267 self.tracks[self.selected_id] = [] 268 continue 269 # Is this a track marker? 270 m = re.search(waypoint_re, line) 271 if m and not ignore: 272 try: 273 tag = m.group(1) 274 x = int(m.group(2)) 275 y = int(m.group(3)) 276 self.tracks[self.selected_id].append((tag, x, y)) 277 continue 278 except ValueError: 279 raise IOException("Invalid coordinate field.", fp.name, i+1) 280 # Is it a property setting? 281 m = re.search(property_re, line) 282 if m: 283 self.properties[m.group(1)] = m.group(2) 284 continue 285 if "map" in self.properties: 286 self.mapfile = self.properties['map'] 287 else: 288 raise IOException("Missing map declaration.", fp.name) 289 fp.close() 290 self.modified = 0 291 def __getitem__(self, n): 292 return self.tracks[self.selected_id][n] 293 def __setitem__(self, n, v): 294 if self.tracks[self.selected_id][n] != v: 295 self.modified += 1 296 self.tracks[self.selected_id][n] = v 297 def add_track(self, name): 298 if name not in self.track_order: 299 self.tracks[name] = [] 300 self.track_order.append(name) 301 if self.selected_id is None: 302 self.selected_id = name 303 self.modified += 1 304 def remove_track(self, name): 305 if name in self.track_order: 306 del self.tracks[name] 307 self.track_order.remove(name) 308 if not self.track_order: 309 self.add_track("JOURNEY") 310 self.modified += 1 311 def rename_track(self, oldname, newname): 312 if oldname in self.tracklist and newname not in self.tracklist: 313 self.tracks[newname] = self.tracks[oldname] 314 self.track_order[self.track_order.index(oldname)] = newname 315 def has_unsaved_changes(self): 316 return self.modified 317 def neighbors(self, x, y): 318 "Return list of neighbors on selected track, enumerated and sorted by distance." 319 neighbors = [] 320 candidates = zip(xrange(len(self.selected_track())), self.selected_track()) 321 candidates.sort(lambda (i1, (a1, x1, y1)), (i2, (a2, x2, y2)): cmp(distance(x, y, x1, y1), distance(x, y, x2, y2))) 322 return candidates 323 def find(self, x, y): 324 "Find all actions at the given pointin in the selected track." 325 candidates = [] 326 for (i, (tag, xt, yt)) in enumerate(self.selected_track()): 327 if x == xt and y == yt: 328 candidates.append(i) 329 return candidates 330 def insert(self, (action, x, y)): 331 "Insert a feature in the selected track." 332 neighbors = self.neighbors(x, y) 333 # There are two or more markers and we're not nearest the end one 334 if len(neighbors) >= 2 and neighbors[0][0] != len(neighbors)-1: 335 closest = neighbors[0] 336 next_closest = neighbors[1] 337 # If the neighbors are adjacent, insert between them 338 if abs(closest[0] - next_closest[0]) == 1: 339 self.selected_track().insert(max(closest[0], next_closest[0]), (action, x, y)) 340 self.modified += 1 341 return 342 # Otherwise, append 343 self.selected_track().append((action, x, y)) 344 self.modified += 1 345 def remove(self, x, y): 346 "Remove a feature from the selected track." 347 found = self.find(x, y) 348 if found: 349 # Prefer to delete the most recent feature 350 track = self.selected_track() 351 self.tracks[self.selected_id] = track[:found[-1]] + track[found[-1]+1:] 352 self.modified += 1 353 def __str__(self): 354 rep = self.mapfile + repr(self.track_order) + "\n" 355 for name in self.track_order: 356 track = self.tracks[name] 357 rep += name + ": " + repr(track) + ":\n" 358 return rep 359 360class ContextPopup: 361 def __init__(self, editor): 362 self.editor = editor 363 self.window = gtk.Window(gtk.WINDOW_POPUP) 364 self.window.set_transient_for(None) 365 self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS) 366 self.window.set_name("trackplacer info") 367 self.frame=gtk.Frame() 368 self.window.add(self.frame) 369 self.frame.show() 370 self.vbox = gtk.VBox(False, 0) 371 self.frame.add(self.vbox) 372 self.vbox.show() 373 self.window.show() 374 self.position = gtk.Label() 375 self.vbox.pack_start(self.position, expand=False, fill=False) 376 self.position.show() 377 def inform(self, x, y): 378 self.position.set_text("At (%d, %d):" % (x, y)) 379 save_selected = self.editor.journey.selected_id 380 local = [] 381 for name in self.editor.journey.track_order: 382 # Gather info 383 self.editor.journey.set_selected_track(name) 384 for (possible, item) in self.editor.journey.neighbors(x, y): 385 if within(x, y, self.editor.box(item)): 386 stagecount = 0 387 for i in xrange(possible): 388 (action, xn, yn) = self.editor.journey[i] 389 if action in segmenters: 390 stagecount += 1 391 local.append((name, possible, self.editor.journey[possible], stagecount)) 392 self.editor.journey.set_selected_track(save_selected) 393 # Display it 394 if local: 395 for (name, index, (action, x, y), sc) in local: 396 legend = "%s at (%d, %d) is %s[%d], stage %d" \ 397 % (action.capitalize(), x,y, name, index, sc+1) 398 label = gtk.Label(legend) 399 label.show() 400 self.vbox.add(label) 401 else: 402 label = gtk.Label("No features") 403 label.show() 404 self.vbox.add(label) 405 def destroy(self): 406 self.window.destroy() 407 408class TrackEditorIcon: 409 def __init__(self, action, path): 410 self.action = action 411 # We need an image for the toolbar... 412 self.image = gtk.Image() 413 self.image.set_from_file(path) 414 # ...and a pixbuf for drawing on the map with. 415 self.icon = gtk.gdk.pixbuf_new_from_file(path) 416 self.icon_width = self.icon.get_width() 417 self.icon_height = self.icon.get_height() 418 def bounding_box(self, x, y): 419 "Return a bounding box for this icon when centered at (x, y)." 420 # The +1 is a slop factor allowing for even-sized icons 421 return (x-self.icon_width/2, y-self.icon_height/2, 422 self.icon_width+1, self.icon_height+1) 423 424class TrackController: 425 "Object for controlling an individual track in the Tracks dialog." 426 def __init__(self, editor, track_id, trackbox, basebutton): 427 self.editor = editor 428 self.track_id = track_id 429 self.hbox = gtk.HBox() 430 trackbox.add(self.hbox) 431 self.hbox.show() 432 self.radiobutton = gtk.RadioButton(basebutton) 433 self.radiobutton.set_active(track_id == editor.journey.selected_id) 434 self.radiobutton.connect("toggled", 435 editor.track_activity_callback, track_id) 436 self.radiobutton.set_tooltip_text("Select %s for editing" % track_id) 437 self.radiobutton.show() 438 self.hbox.add(self.radiobutton) 439 self.checkbox = gtk.CheckButton() 440 self.checkbox.set_active(track_id in editor.visible_set) 441 self.checkbox.connect("toggled", 442 editor.track_visibility_callback, track_id) 443 self.hbox.add(self.checkbox) 444 self.checkbox.set_tooltip_text("Toggle visibility of %s" % track_id) 445 self.checkbox.show() 446 self.rename = gtk.Entry() 447 self.rename.set_text(track_id) 448 self.rename.connect("activate", self.track_rename_handler, track_id) 449 self.rename.set_tooltip_text("Change name of track %s" % track_id) 450 self.rename.show() 451 self.hbox.add(self.rename) 452 # We really should have been able to do this: 453 # self.deleter = gtk.Button(stock=gtk.STOCK_DELETE, label="") 454 # Instead, we have to writhe and faint in coils because the 455 # stock argument forces the label. 456 self.deleter = gtk.Button() 457 delimage = gtk.Image() 458 delimage.set_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_SMALL_TOOLBAR) 459 bbox = gtk.HBox() 460 self.deleter.add(bbox) 461 bbox.add(delimage) 462 delimage.show() 463 bbox.show() 464 465 self.deleter.connect("clicked", self.track_delete_handler, track_id) 466 self.hbox.add(self.deleter) 467 self.deleter.set_tooltip_text("Delete track %s" % track_id) 468 self.deleter.show() 469 editor.controller[track_id] = self 470 def track_delete_handler(self, w, track_id): 471 if track_id in self.editor.visible_set: 472 self.editor.visible_set.remove(track_id) 473 if track_id == self.editor.journey.selected_id: 474 self.editor.track_select(w, self.editor.visible_set[-1]) 475 # FIXME: This redraw fails when we delete the last track. 476 self.editor.redraw(self.editor.drawing_area) 477 self.editor.journey.remove_track(track_id) 478 self.hbox.hide() 479 del self.editor.controller[track_id] 480 def track_rename_handler(self, w, track_id): 481 editor.journey.rename(track_id, w.get_text()) 482 self.editor.controller[w.get_text()] = self.editor.controller[track_id] 483 del self.editor.controller[track_id] 484 485class TracksEditor: 486 def __init__(self, path=None, verbose=False, force_save=False): 487 self.verbose = verbose 488 self.force_save = force_save 489 # Initialize our info about the map and track 490 self.journey = JourneyTracks() 491 self.last_read = None 492 self.journey.read(path) 493 self.time_last_io = time.time() 494 if path.endswith(".cfg"): 495 self.last_read = path 496 self.log("Initial track is %s" % self.journey) 497 self.action = "JOURNEY" 498 self.selected = None 499 self.visible_set = self.journey.track_order[:] 500 self.context_popup = None 501 self.pixmap = None # Backing pixmap for drawing area 502 503 # Grab the map into a pixmap 504 self.log("about to read map %s" % self.journey.mapfile) 505 try: 506 self.map = gtk.gdk.pixbuf_new_from_file(self.journey.mapfile) 507 self.map_width = self.map.get_width() 508 self.map_height = self.map.get_height() 509 self.map = self.map.render_pixmap_and_mask()[0] 510 except: 511 self.fatal_error("Error while reading background map %s" % self.journey.mapfile) 512 # Now get the icons we'll need for scribbling on the map with. 513 try: 514 self.selected_dictionary = {} 515 for (action, path) in selected_icon_dictionary.items(): 516 icon = TrackEditorIcon(action, path) 517 self.log("selected %s icon has size %d, %d" % \ 518 (action, icon.icon_width, icon.icon_height)) 519 self.selected_dictionary[action] = icon 520 self.unselected_dictionary = {} 521 for (action, path) in unselected_icon_dictionary.items(): 522 icon = TrackEditorIcon(action, path) 523 self.log("unselected %s icon has size %d, %d" % \ 524 (action, icon.icon_width, icon.icon_height)) 525 self.unselected_dictionary[action] = icon 526 except: 527 self.fatal_error("error while reading icons") 528 529 # Window-layout time 530 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) 531 self.window.set_name ("trackplacer") 532 533 vbox = gtk.VBox(False, 0) 534 self.window.add(vbox) 535 vbox.show() 536 537 self.window.connect("destroy", lambda w: gtk.main_quit()) 538 539 # Set up toolbar style 540 toolbar = gtk.Toolbar() 541 toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL) 542 toolbar.set_style(gtk.TOOLBAR_BOTH) 543 toolbar.set_border_width(1) 544 vbox.pack_start(toolbar, expand = False) 545 toolbar.show() 546 547 # Toolbar widget has a fit when we try to pack these separately. 548 radiobox1 = gtk.ToolItem() 549 radiobox = gtk.HBox() 550 radiobox1.add(radiobox) 551 radiobox1.show() 552 radiobox.show() 553 toolbar.insert(radiobox1, -1) 554 555 # Marker selection 556 basebutton = None 557 for action in icon_presentation_order: 558 icon = self.selected_dictionary[action] 559 button = gtk.RadioButton(basebutton) 560 bbox = gtk.HBox() 561 button.add(bbox) 562 bbox.add(icon.image) 563 icon.image.show() 564 bbox.show() 565 if not basebutton: 566 button.set_active(True) 567 basebutton = button 568 button.connect("toggled", self.button_callback, icon.action) 569 radiobox.pack_start(button, padding=7) 570 button.show() 571 button.set_tooltip_text("Place %s markers" % action.lower()) 572 573 # The delete button and its label 574 button = gtk.RadioButton(button) 575 delimage = gtk.Image() 576 delimage.set_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_SMALL_TOOLBAR) 577 bbox = gtk.HBox() 578 button.add(bbox) 579 bbox.add(delimage) 580 delimage.show() 581 bbox.show() 582 button.connect("toggled", self.button_callback, "DELETE") 583 radiobox.pack_start(button, padding=7) 584 button.show() 585 button.set_tooltip_text("Remove markers") 586 587 # The copy button and its label 588 button = gtk.RadioButton(button) 589 copyimage = gtk.Image() 590 copyimage.set_from_stock(gtk.STOCK_CONVERT, gtk.ICON_SIZE_SMALL_TOOLBAR) 591 bbox = gtk.HBox() 592 button.add(bbox) 593 bbox.add(copyimage) 594 copyimage.show() 595 bbox.show() 596 button.connect("toggled", self.button_callback, "COPY") 597 radiobox.pack_start(button, padding=7) 598 button.show() 599 button.set_tooltip_text("Copy marker from an unselected track") 600 601 # Sigh - causes elements to jumop around in the toolbar, 602 # because when it's not there the application wants the 603 # extra space for buttons. 604 #self.coordwin = gtk.Label("") 605 #coordwrapper = gtk.ToolItem() 606 #coordwrapper.add(self.coordwin) 607 #toolbar.add(coordwrapper) 608 #coordwrapper.set_expand(True) 609 #self.coordwin.show() 610 #coordwrapper.show() 611 612 spacer = gtk.SeparatorToolItem() 613 toolbar.add(spacer) 614 spacer.set_draw(False) 615 spacer.set_expand(True) 616 spacer.show() 617 618 quit = gtk.ToolButton(gtk.STOCK_QUIT) 619 toolbar.insert(quit, -1) 620 quit.set_tooltip_text("Leave this program.") 621 quit.connect("clicked", self.quit) 622 quit.show() 623 624 save = gtk.ToolButton(gtk.STOCK_SAVE) 625 toolbar.insert(save, -1) 626 save.set_tooltip_text("Save journey tracks.") 627 save.connect("clicked", self.save_handler) 628 save.show() 629 630 properties = gtk.ToolButton(gtk.STOCK_PROPERTIES) 631 toolbar.insert(properties, -1) 632 properties.set_tooltip_text("St properties of the tracks.") 633 properties.connect("clicked", self.properties_handler) 634 properties.show() 635 636 animate = gtk.ToolButton(gtk.STOCK_REFRESH) 637 animate.set_label(label="Animate") 638 toolbar.insert(animate, -1) 639 animate.set_tooltip_text("Animate tracks as in story parts.") 640 animate.connect("clicked", self.animate_handler) 641 animate.show() 642 643 tracks = gtk.ToolButton(gtk.STOCK_INDEX) 644 tracks.set_label(label="Tracks") 645 toolbar.insert(tracks, -1) 646 tracks.set_tooltip_text("Add, edit, delete and rename tracks.") 647 tracks.connect("clicked", self.tracks_handler) 648 tracks.show() 649 650 help = gtk.ToolButton(gtk.STOCK_HELP) 651 toolbar.insert(help, -1) 652 help.set_tooltip_text("Get command help for this program.") 653 help.connect("clicked", self.help_handler) 654 help.show() 655 656 about = gtk.ToolButton(gtk.STOCK_ABOUT) 657 toolbar.insert(about, -1) 658 about.set_tooltip_text("See credits for this program.") 659 about.connect("clicked", self.about_handler) 660 about.show() 661 662 # Create the drawing area on a viewport that scrolls, if needed. 663 self.drawing_area = gtk.DrawingArea() 664 self.drawing_area.set_size_request(self.map_width, self.map_height) 665 screen_width = gtk.gdk.screen_width() 666 screen_height = gtk.gdk.screen_height() 667 if self.map_width < 0.75 * screen_width and self.map_height < 0.75 * screen_width: 668 # Screen is large relative to the image. Grab enough 669 # space to display the entire map. and never scroll. 670 # There should be enough space around the edges for window 671 # decorations, task bars, etc. 672 vbox.pack_start(self.drawing_area, expand=True, fill=True, padding=0) 673 self.drawing_area.show() 674 else: 675 # Screen is small. Grab all the space the window manager will 676 # give us and deal with scrolling. 677 scroller = gtk.ScrolledWindow() 678 scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 679 scroller.add_with_viewport(self.drawing_area) 680 vbox.pack_start(scroller) 681 self.window.maximize() 682 self.drawing_area.show() 683 scroller.show() 684 685 # Signals used to handle backing pixmap 686 self.drawing_area.connect("expose_event", self.expose_event) 687 self.drawing_area.connect("configure_event", self.configure_event) 688 689 # Event signals 690 self.drawing_area.connect("motion_notify_event", self.motion_notify_event) 691 self.drawing_area.connect("button_press_event", self.button_press_event) 692 693 self.drawing_area.connect("button_release_event", self.button_release_event) 694 695 self.drawing_area.connect("leave_notify_event", self.leave_area_event) 696 697 self.drawing_area.set_events(gtk.gdk.EXPOSURE_MASK 698 | gtk.gdk.LEAVE_NOTIFY_MASK 699 | gtk.gdk.BUTTON_PRESS_MASK 700 | gtk.gdk.BUTTON_RELEASE_MASK 701 | gtk.gdk.POINTER_MOTION_MASK 702 | gtk.gdk.POINTER_MOTION_HINT_MASK) 703 704 705 self.window.show() 706 gtk.main() 707 self.log("initialization successful") 708 709 def button_callback(self, widget, data=None): 710 "Radio button callback, changes selected editing action." 711 if widget.get_active(): 712 self.action = data 713 714 def refresh_map(self, x=0, y=0, xs=-1, ys=-1): 715 "Refresh part of the drawing area with the appropriate map rectangle." 716 self.log("Refreshing map in (%d, %d, %d, %d, %d, %d}" % (x,y,x,y,xs,ys)) 717 self.pixmap.draw_drawable(self.default_gc, self.map, x, y, x, y, xs, ys) 718 719 def box(self, (action, x, y)): 720 "Compute the bounding box for an icon of type ACTION at X, Y." 721 # Assumes selected and unselected icons are the same size 722 return self.selected_dictionary[action].bounding_box(x, y) 723 724 def snap_to(self, x, y): 725 "Snap a location to the nearest feature on the selected track whose bounding box holds it." 726 self.log("Neighbors of %d, %d are %s" % (x, y, self.journey.neighbors(x, y))) 727 for (i, item) in self.journey.neighbors(x, y): 728 if within(x, y, self.box(item)): 729 return i 730 else: 731 return None 732 733 def neighbors(self, (action, x, y)): 734 "Return all track items with bounding boxes overlapping this one:" 735 rect = self.selected_dictionary[action].bounding_box(x, y) 736 return filter(lambda item: overlaps(rect, self.box(item)), 737 self.journey.selected_track()) 738 739 def erase_feature(self, widget, (action, x, y)): 740 "Erase specified (active) icon from the map." 741 # Erase all nearby features that might have been damaged. 742 save_select = self.journey.selected_id 743 for (id, track) in self.journey.tracks.items(): 744 if id not in self.visible_set: 745 continue 746 self.journey.set_selected_track(id) 747 neighbors = self.neighbors((action, x, y)) 748 for (na, nx, ny) in neighbors: 749 rect = self.box((na, nx, ny)) 750 self.log("Erasing action=%s, dest=%s" % (na, rect)) 751 self.refresh_map(*rect) 752 widget.queue_draw_area(*rect) 753 # Redraw all nearby features except what we're erasing. 754 for (na, nx, ny) in neighbors: 755 if x != nx and y != ny: 756 self.log("Redrawing action=%s" % ((na, nx, ny),)) 757 self.draw_feature(widget, 758 (na, nx, ny), 759 save_select == self.journey.selected_id) 760 self.journey.set_selected_track(save_select) 761 762 def draw_feature(self, widget, (action, x, y), selected): 763 "Draw specified icon on the map." 764 rect = self.box((action, x, y)) 765 self.log("Drawing action=%s (%s), dest=%s" % (action, selected, rect)) 766 if selected: 767 icon = self.selected_dictionary[action].icon 768 else: 769 icon = self.unselected_dictionary[action].icon 770 self.pixmap.draw_pixbuf(self.default_gc, icon, 0, 0, *rect) 771 widget.queue_draw_area(*rect) 772 773 def flush(self, widget): 774 "Force pending events out." 775 self.expose_event(widget) 776 while gtk.events_pending(): 777 gtk.main_iteration(False) 778 779 def redraw(self, widget, delay=0): 780 "Redraw the map and tracks." 781 self.refresh_map() 782 for track_id in self.journey.track_order: 783 if track_id not in self.visible_set: 784 continue 785 for item in self.journey.tracks[track_id]: 786 self.draw_feature(widget, item, track_id == self.journey.selected_id) 787 if delay: 788 time.sleep(delay) 789 self.flush(widget) 790 # To ensure items on selected track are on top, redraw them 791 if self.journey.track_order: 792 for item in self.journey.selected_track(): 793 self.draw_feature(widget, item, True) 794 self.flush(widget) 795 796 def configure_event(self, widget, event): 797 "Create a new backing pixmap of the appropriate size." 798 x, y, width, height = widget.get_allocation() 799 self.pixmap = gtk.gdk.Pixmap(widget.window, width, height) 800 self.default_gc = self.drawing_area.get_style().fg_gc[gtk.STATE_NORMAL] 801 self.redraw(widget) 802 return True 803 804 def expose_event(self, widget, event=None): 805 "Redraw the screen from the backing pixmap" 806 if event: 807 x , y, width, height = event.area 808 else: 809 x, y, width, height = widget.get_allocation() 810 widget.window.draw_drawable(self.default_gc, 811 self.pixmap, x, y, x, y, width, height) 812 return False 813 814 def button_press_event(self, widget, event): 815 if self.pixmap is None: 816 return 817 if self.journey.selected_track() is None: 818 w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) 819 w.set_markup("No track to edit!") 820 w.run() 821 return 822 # Pick up state information whatever button is pressed 823 a = self.action 824 x = int(event.x) 825 y = int(event.y) 826 self.selected = self.snap_to(x, y) 827 # Event button 1 - draw 828 if event.button == 1: 829 # Skip the redraw in half the cases 830 self.log("Action %s at (%d, %d): feature = %s" % (self.action, x, y, self.selected)) 831 if self.selected == None and self.action == "COPY": 832 save_selected = self.journey.selected_id 833 most_recent = None 834 for name in self.journey.track_order: 835 if name != save_selected: 836 self.journey.set_selected_track(name) 837 possible = self.snap_to(x, y) 838 if possible is not None: 839 print "Found possible on", name 840 most_recent = (name, possible, self.journey[possible]) 841 self.journey.set_selected_track(save_selected) 842 if most_recent: 843 (nn, np, (an, xn, yn)) = most_recent 844 self.log("Copy feature: %s[%d] = %s" % (nn, np, (an,xn,yn))) 845 (a, x, y) = (an, xn, yn) 846 else: 847 return 848 if (self.selected == None) and (a == "DELETE"): 849 return 850 if (self.selected != None) and (a != "DELETE"): 851 return 852 # Actual drawing and mutation of the journey track happens here 853 if not self.selected and a != "DELETE": 854 self.draw_feature(widget, (a, x, y), True) 855 self.journey.insert((a, x, y)) 856 elif self.selected != None and a == "DELETE": 857 (a, x, y) = self.journey[self.selected] 858 self.log("Deletion snapped to feature %d %s" % (self.selected,(a,x,y))) 859 self.erase_feature(widget, (a, x, y)) 860 self.journey.remove(x, y) 861 self.log("Tracks are %s" % self.journey) 862 # Event button 3 - query 863 if event.button == 3: 864 self.context_popup = ContextPopup(self) 865 self.context_popup.inform(x, y) 866 return True 867 868 def button_release_event(self, widget, event): 869 if self.context_popup is not None: 870 self.context_popup.destroy() 871 872 def motion_notify_event(self, widget, event): 873 if event.is_hint: 874 x, y, state = event.window.get_pointer() 875 else: 876 x = event.x 877 y = event.y 878 #self.coordwin.set_text("(%d, %d)" % (x, y)) 879 state = event.state 880 881 # This code enables dragging icons wit h the middle button. 882 if state & gtk.gdk.BUTTON2_MASK and self.pixmap != None: 883 if self.selected is not None: 884 (action, lx, ly) = self.journey[self.selected] 885 self.erase_feature(widget, (action, lx, ly)) 886 self.journey[self.selected] = (action, x, y) 887 self.journey.modified += 1 888 self.draw_feature(widget, (action, x, y), True) 889 self.log("Tracks are %s" % self.journey) 890 return True 891 892 def leave_area_event(self, w, e): 893 if self.context_popup: 894 self.context_popup.destroy() 895 #self.coordwin.set_text("") 896 897 def quit(self, w): 898 if self.journey.has_unsaved_changes(): 899 self.quit_check = gtk.Dialog(title="Really quit?", 900 parent=None, 901 flags=gtk.DIALOG_MODAL, 902 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 903 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) 904 label = gtk.Label("Track has unsaved changes. OK to quit?") 905 self.quit_check.vbox.pack_start(label) 906 label.show() 907 response = self.quit_check.run() 908 self.quit_check.destroy() 909 if response == gtk.RESPONSE_ACCEPT: 910 sys.exit(0) 911 else: 912 sys.exit(0) 913 914 def save_handler(self, w): 915 "Save track data," 916 if not self.journey.has_unsaved_changes() and not self.force_save: 917 w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, 918 flags=gtk.DIALOG_DESTROY_WITH_PARENT, 919 buttons=gtk.BUTTONS_OK) 920 w.set_markup("You have no unsaved changes.") 921 w.run() 922 w.destroy() 923 else: 924 # Request save file name 925 dialog = gtk.FileChooserDialog("Save track macros", 926 None, 927 gtk.FILE_CHOOSER_ACTION_SAVE, 928 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 929 gtk.STOCK_SAVE, gtk.RESPONSE_OK)) 930 dialog.set_default_response(gtk.RESPONSE_CANCEL) 931 if self.last_read: 932 dialog.set_filename(self.last_read) 933 dialog.set_show_hidden(False) 934 935 sfilter = gtk.FileFilter() 936 sfilter.set_name("Track files") 937 sfilter.add_pattern("*.cfg") 938 dialog.add_filter(sfilter) 939 940 response = dialog.run() 941 filename = dialog.get_filename() 942 dialog.destroy() 943 if response == gtk.RESPONSE_CANCEL: 944 return 945 946 # Relativize file path to current directory 947 if filename.startswith(os.getcwd() + os.sep): 948 filename = filename[len(os.getcwd())+1:] 949 950 # Request overwrite confirmation in some circumstances 951 confirmation_required = None 952 if os.path.exists(filename): 953 if filename != self.last_read: 954 confirmation_required = "You have requested saving "\ 955 "to a file other than %s, " \ 956 "and that file already exists." \ 957 % self.last_read 958 elif os.stat(filename).st_mtime > self.time_last_io: 959 confirmation_required = "File has changed "\ 960 "since last read or written." 961 if confirmation_required: 962 confirmation_required += "\nReally overwrite %s?" % filename 963 save_check = gtk.Dialog(title="Really overwrite?", 964 parent=None, 965 flags=gtk.DIALOG_MODAL, 966 buttons=(gtk.STOCK_CANCEL, 967 gtk.RESPONSE_REJECT, 968 gtk.STOCK_OK, 969 gtk.RESPONSE_ACCEPT)) 970 label = gtk.Label(confirmation_required) 971 save_check.vbox.pack_start(label) 972 label.show() 973 response = save_check.run() 974 save_check.destroy() 975 if response == gtk.RESPONSE_REJECT: 976 return 977 978 # Actual I/O 979 self.log("Writing track data to %s" % filename) 980 try: 981 self.journey.write(filename) 982 if not self.journey.mapfile: 983 self.journey.mapfile = filename 984 self.time_last_io = time.time() 985 except IOError: 986 w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, 987 flags=gtk.DIALOG_DESTROY_WITH_PARENT, 988 buttons=gtk.BUTTONS_OK) 989 w.set_markup("Cannot write" + filename) 990 w.run() 991 w.destroy() 992 993 def help_handler(self, w): 994 "Display help." 995 w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, 996 flags=gtk.DIALOG_DESTROY_WITH_PARENT, 997 buttons=gtk.BUTTONS_OK) 998 w.set_markup(gui_help) 999 w.run() 1000 w.destroy() 1001 1002 def about_handler(self, w): 1003 "Display about information." 1004 w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, 1005 flags=gtk.DIALOG_DESTROY_WITH_PARENT, 1006 buttons=gtk.BUTTONS_OK) 1007 w.set_markup(gui_about) 1008 w.run() 1009 w.destroy() 1010 1011 def tracks_handler(self, w): 1012 "Modify the visible set of tracks." 1013 self.visibility = gtk.Dialog(title="Edit track visibility", 1014 buttons=(gtk.STOCK_CLOSE, 1015 gtk.RESPONSE_ACCEPT)) 1016 label = gtk.Label("The radiobuttons select a track for editing.") 1017 self.visibility.vbox.pack_start(label) 1018 self.visibility_toggles = {} 1019 label.show() 1020 label = gtk.Label("The checkbuttons toggle the visibility of tracks.") 1021 self.visibility.vbox.pack_start(label) 1022 label.show() 1023 self.controller = {} 1024 basebutton = None 1025 self.trackbox = gtk.VBox() 1026 self.visibility.vbox.add(self.trackbox) 1027 self.trackbox.show() 1028 basebutton = gtk.RadioButton() # Dummy, don't show it. 1029 for (i, track_id) in enumerate(self.journey.track_order): 1030 TrackController(self, track_id, self.trackbox, basebutton) 1031 movebox = gtk.HBox() 1032 label = gtk.Label("The up and down buttons change the track order.") 1033 self.visibility.vbox.pack_start(label) 1034 label.show() 1035 upbutton = gtk.Button(stock=gtk.STOCK_GO_UP) 1036 movebox.pack_start(upbutton, expand=True, fill=True) 1037 upbutton.connect("clicked", lambda w: self.track_move(backward=True)) 1038 upbutton.show() 1039 downbutton = gtk.Button(stock=gtk.STOCK_GO_DOWN) 1040 movebox.pack_start(downbutton, expand=True, fill=True) 1041 downbutton.connect("clicked", lambda w: self.track_move(backward=False)) 1042 downbutton.show() 1043 movebox.show() 1044 addbox = gtk.HBox() 1045 addbox.show() 1046 addlabel = gtk.Label("Add New Track:") 1047 addlabel.show() 1048 addbox.add(addlabel) 1049 addentry = gtk.Entry() 1050 addentry.show() 1051 addbox.add(addentry) 1052 addentry.connect("activate", self.track_add_callback, basebutton) 1053 self.visibility.vbox.add(movebox) 1054 self.visibility.vbox.add(addbox) 1055 self.visibility.connect("response", self.track_visibility_revert) 1056 self.visibility.show() 1057 def track_move(self, backward): 1058 where = self.journey.track_order.index(self.journey.selected_id) 1059 if backward: 1060 where += (len(self.journey.track_order) - 1) 1061 else: 1062 where += 1 1063 where %= len(self.journey.track_order) 1064 self.journey.track_order.remove(self.journey.selected_id) 1065 self.journey.track_order.insert(where, self.journey.selected_id) 1066 self.trackbox.reorder_child(self.controller[self.journey.selected_id].hbox, where) 1067 def track_select(self, w, track_id): 1068 "Make the specified track the selected one for editing." 1069 self.journey.set_selected_track(track_id) 1070 self.controller[track_id].checkbox.set_active(True) 1071 self.controller[track_id].radiobutton.set_active(True) 1072 if track_id not in self.visible_set: 1073 self.track_visibility_callback(self.controller[track_id], track_id) 1074 else: 1075 self.redraw(self.drawing_area) 1076 def track_activity_callback(self, w, track_id): 1077 "Called (twice) when a track activity radiobutton is toggled." 1078 if w.get_active(): 1079 self.track_select(w, track_id) 1080 def track_visibility_callback(self, w, track_id): 1081 "Called when a track visibility checkbutton is toggled." 1082 if len(self.visible_set) <= 1 and track_id in self.visible_set: 1083 w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, 1084 flags=gtk.DIALOG_DESTROY_WITH_PARENT, 1085 buttons=gtk.BUTTONS_OK) 1086 w.set_markup("At least one track must remain visible.") 1087 self.controller[track_id].checkbox.set_active(True) 1088 w.run() 1089 w.destroy() 1090 return 1091 self.log("Toggling visibility of %s" % track_id) 1092 if track_id in self.visible_set: 1093 self.visible_set.remove(track_id) 1094 else: 1095 self.visible_set.append(track_id) 1096 self.log("Visibility set is now %s" % self.visible_set) 1097 if self.journey.selected_id not in self.visible_set: 1098 self.controller[track_id].radiobutton.set_active(False) 1099 self.journey.set_selected_track(self.visible_set[-1]) 1100 self.controller[self.visible_set[-1]].radiobutton.set_active(True) 1101 else: 1102 self.redraw(self.drawing_area) 1103 def track_add_callback(self, w, basebutton): 1104 "Add a new track, and the controller for it, and select it." 1105 track_id = w.get_text() 1106 w.set_text("") 1107 TrackController(self, track_id, self.trackbox, basebutton) 1108 self.journey.add_track(track_id) 1109 self.track_select(w, track_id) 1110 def track_visibility_revert(self, w, response_id): 1111 "On response or window distruction, restore visibility set." 1112 self.visible_set = self.journey.track_order 1113 self.redraw(self.drawing_area) 1114 self.visibility.destroy() 1115 1116 def properties_handler(self, w): 1117 "Display a dialog for editing track properties." 1118 w = gtk.Dialog(title="Track properties editor", 1119 parent=None, 1120 flags=gtk.DIALOG_MODAL, 1121 buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, 1122 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) 1123 label = gtk.Label("You can enter a key/value pair for a new property on the last line.") 1124 label.show() 1125 w.vbox.pack_start(label) 1126 table = gtk.Table(len(self.journey.properties)+1, 2) 1127 table.show() 1128 w.vbox.pack_start(table) 1129 keys = self.journey.properties.keys() 1130 keys.sort() 1131 labels = [] 1132 entries = [] 1133 for (i, key) in enumerate(keys): 1134 labels.append(gtk.Label(key)) 1135 labels[-1].show() 1136 table.attach(labels[-1], 0, 1, i, i+1) 1137 entries.append(gtk.Entry()) 1138 entries[-1].set_text(self.journey.properties[key]) 1139 entries[-1].set_width_chars(50) 1140 entries[-1].show() 1141 table.attach(entries[-1], 1, 2, i, i+1) 1142 new_key = gtk.Entry() 1143 new_key.set_width_chars(12) 1144 new_key.show() 1145 table.attach(new_key, 0, 1, len(keys)+1, len(keys)+2) 1146 new_value = gtk.Entry() 1147 new_value.set_width_chars(50) 1148 table.attach(new_value, 1, 2, len(keys)+1, len(keys)+2) 1149 new_value.show() 1150 response = w.run() 1151 w.destroy() 1152 if response == gtk.RESPONSE_ACCEPT: 1153 for (label, entry) in zip(labels, entries): 1154 self.journey.properties[label.get_text()] = entry.get_text() 1155 if new_key.get_text() and new_label.get_text(): 1156 self.journey.properties[new_key.get_text()] = new_entry.get_text() 1157 1158 def animate_handler(self, w): 1159 "Animate dot placing as though on a storyboard." 1160 self.refresh_map() 1161 self.expose_event(self.drawing_area) 1162 self.redraw(self.drawing_area, 0.5) 1163 1164 def log(self, msg): 1165 "Debugging report." 1166 if self.verbose: 1167 print >>sys.stderr, "trackplacer:", msg 1168 1169 def fatal_error(self, msg): 1170 "Notify user of error and die." 1171 w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) 1172 w.set_markup(msg) 1173 w.run() 1174 sys.exit(1) 1175 1176if __name__ == "__main__": 1177 (options, arguments) = getopt.getopt(sys.argv[1:], "d:fhv?", 1178 ['directory=', 'force', 'help', 'verbose']) 1179 verbose = force_save = False 1180 top = None 1181 for (opt, val) in options: 1182 if opt in ('-d', '--directory'): 1183 top = val 1184 elif opt in ('-f', '--force'): 1185 force_save = True 1186 elif opt in ('-?', '-h', '--help'): 1187 print __doc__ 1188 sys.exit(0) 1189 elif opt in ('-v', '--verbose'): 1190 verbose = True 1191 1192 here = os.getcwd() 1193 if top: 1194 os.chdir(top) 1195 else: 1196 wesnoth.wmltools.pop_to_top("trackplacer") 1197 if arguments: 1198 try: 1199 filename = os.path.join(here, arguments[0]) 1200 # Relativize file path to current directory 1201 if filename.startswith(os.getcwd() + os.sep): 1202 filename = filename[len(os.getcwd())+1:] 1203 TracksEditor(path=filename, verbose=verbose, force_save=force_save) 1204 except IOException, e: 1205 if e.lineno: 1206 sys.stderr.write(('"%s", line %d: ' % (e.path, e.lineno)) + e.message + "\n") 1207 else: 1208 sys.stderr.write(e.path + ": " + e.message + "\n") 1209 else: 1210 while True: 1211 try: 1212 dialog = gtk.FileChooserDialog("Open track file", 1213 None, 1214 gtk.FILE_CHOOSER_ACTION_OPEN, 1215 (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 1216 gtk.STOCK_OPEN, gtk.RESPONSE_OK)) 1217 dialog.set_default_response(gtk.RESPONSE_OK) 1218 dialog.set_filename(default_map) 1219 dialog.set_show_hidden(False) 1220 1221 ofilter = gtk.FileFilter() 1222 ofilter.set_name("Images and Tracks") 1223 ofilter.add_mime_type("image/png") 1224 ofilter.add_mime_type("image/jpeg") 1225 ofilter.add_mime_type("image/gif") 1226 ofilter.add_pattern("*.png") 1227 ofilter.add_pattern("*.jpg") 1228 ofilter.add_pattern("*.gif") 1229 ofilter.add_pattern("*.tif") 1230 ofilter.add_pattern("*.xpm") 1231 ofilter.add_pattern("*.cfg") 1232 dialog.add_filter(ofilter) 1233 1234 ofilter = gtk.FileFilter() 1235 ofilter.set_name("Images only") 1236 ofilter.add_mime_type("image/png") 1237 ofilter.add_mime_type("image/jpeg") 1238 ofilter.add_mime_type("image/gif") 1239 ofilter.add_pattern("*.png") 1240 ofilter.add_pattern("*.jpg") 1241 ofilter.add_pattern("*.gif") 1242 ofilter.add_pattern("*.tif") 1243 ofilter.add_pattern("*.xpm") 1244 dialog.add_filter(ofilter) 1245 1246 ofilter = gtk.FileFilter() 1247 ofilter.set_name("Tracks only") 1248 ofilter.add_pattern("*.cfg") 1249 dialog.add_filter(ofilter) 1250 1251 response = dialog.run() 1252 if response == gtk.RESPONSE_OK: 1253 filename = dialog.get_filename() 1254 elif response == gtk.RESPONSE_CANCEL: 1255 sys.exit(0) 1256 dialog.destroy() 1257 1258 # Relativize file path to current directory 1259 if filename.startswith(os.getcwd() + os.sep): 1260 filename = filename[len(os.getcwd())+1:] 1261 1262 TracksEditor(filename, verbose=verbose, force_save=force_save) 1263 except IOException, e: 1264 w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, 1265 flags=gtk.DIALOG_DESTROY_WITH_PARENT, 1266 buttons=gtk.BUTTONS_OK) 1267 if e.lineno: 1268 errloc = '"%s", line %d:' % (e.path, e.lineno) 1269 # Emacs friendliness 1270 sys.stderr.write(errloc + " " + e.message + "\n") 1271 else: 1272 errloc = e.path + ":" 1273 w.set_markup(errloc + "\n\n" + e.message) 1274 w.run() 1275 w.destroy() 1276