#!/usr/bin/env python ''' trackplacer -- map journey track editor. usage: trackplacer [-vh?] [filename] A journey is an object containing a map file name and a (possibly empty) list of tracks, each with a name and each consisting of a sequence of track markers. This program exists to visually edit journeys represented as specially delimited sections in in .cfg files. If the .cfg filename is not specified, trackplacer will enter a loop in which it repeatedly pops up a file selector. Canceling the file selecct ends the program; Selecting a file takes you to a main screen. For command help on the main screen, click the Help button. Can be started with a map image, in which case we are editing a new journey. Can be started with a .cfg file, in which case iit will look for track information enclosed in special comments that look like this: # trackplacer: tracks begin # trackplacer: tracks end trackplacer will not alter anything it finds outside these comments, and will always enclose everything it writes in them. Special comments may appear in the track section, looking like this: # trackplacer: = These set properties that trackplacer may use. At present there is only one such property: "map", which records the name of the mapfile on which your track is laid. Normally, trackplacer assumes it is running within a Battle for Wesnoth source tree and changes directory to the root of the tree. Paths saved in track files are relative to the tree root. All pathnames in help and error messages are also relativized to that root. The -v option enables verbose logging to standard error. The -d option sets the root directory to use. The -h or -? options display this summary. For details on theediting controls, click the Help button in the trackplacer GUI. ''' gui_help = '''\ This is trackplacer, an editor for visually editing sets of journey tracks on Battle For Wesnoth maps. You 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. The radio buttons near the top left corner control which icon is placed by a left click, except that the when the trashcan is selected a left click deletes already-placed icons. Every time you place an icon, it is added to the currently selected track. The 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. The 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 edited. The 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. The 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. The Tracks button pops up a list of checkboxes, one for each track. You can shange the state of the checkboxes to control which tracks are visible. The radiobuttons can be used to select a track for editing. The Help button displays this message. The Quit button ends your session, asking for confirmation if you have unsaved changes. Design and implementation by Eric S. Raymond, October 2008. ''' import sys, os, re, math, time, exceptions, getopt import pygtk pygtk.require('2.0') import gtk import wesnoth.wmltools # All dependencies on the shape of the data tree live here # The code does no semantic interpretation of these icons at all; # to add new ones, just fill in a dictionary entry. imagedir = "data/core/images/" default_map = imagedir + "maps/wesnoth.png" selected_icon_dictionary = { "JOURNEY": imagedir + "misc/new-journey.png", "BATTLE": imagedir + "misc/new-battle.png", "REST": imagedir + "misc/flag-red.png", } unselected_icon_dictionary = { "JOURNEY": imagedir + "misc/dot-white.png", "BATTLE": imagedir + "misc/cross-white.png", "REST": imagedir + "misc/flag-white.png", } icon_presentation_order = ("JOURNEY", "BATTLE", "REST") segmenters = ("BATTLE","REST") class IOException(exceptions.Exception): "Exception thrown while reading a track file." def __init__(self, message, path, lineno=None): self.message = message self.path = path self.lineno = lineno # Basic functions for bashing points and rectangles def distance(x1, y1, x2, y2): "Euclidean distance." return math.sqrt((x1 - x2)**2 + abs(y1 - y2)**2) def within(x, y, (l, t, r, d)): "Is point within specified rectangle?" if x >= l and x < l + r and y >= t and y < t + d: return True return False def overlaps((x, y, xd, yd), rect): "Do two rectangles overlap?" return within(x, y, rect) or within(x+xd-1, y, rect) or \ within(x, y+yd-1, rect) or within(x+xd-1, y+yd+1, rect) class JourneyTracks: "Represent a set of named journey tracks on a map." def __init__(self): self.mapfile = None # Map background of the journey self.tracks = {} # Dict of lists of (action, x, y) tuples self.selected_id = None self.modifications = 0 self.track_list = [] self.properties = {} self.modified = False self.before = self.after = "" def selected_track(self): "Select a track for modification" return self.tracks[self.selected_id] def set_selected_track(self, name): self.selected_id = name def write(self, fp): "Record a set of named journey tracks." if fp.name.endswith(".cfg"): fp.write(self.before) fp.write("# trackplacer: tracks begin\n\n") fp.write("# Hand-hack this section strictly at your own risk.\n\n") fp.write("#\n") if not self.before and not self.after: fp.write("#\n# wmllint: no translatables\n\n") for (key, val) in self.properties.items(): fp.write("# trackplacer: %s=%s\n" % (key, val)) fp.write("#\n") for (name, track) in self.tracks.items(): index_tuples = zip(range(len(track)), track) index_tuples = filter(lambda (i, (a, x, y)): a in segmenters, index_tuples) endpoints = map(lambda (i, t): i, index_tuples) if track[-1][0] not in segmenters: endpoints.append(len(track)-1) for (i, e) in enumerate(endpoints): fp.write("#define %s_STAGE_%d\n" % (name, i+1,)) for j in range(0, e+1): age="OLD" if i == 0 or j > endpoints[i-1]: age = "NEW" waypoint = (age,) + tuple(track[j]) fp.write(" {%s_%s %d %d}\n" % waypoint) fp.write("#enddef\n\n") fp.write("#define %s_COMPLETE\n" % name) for j in range(len(track)): waypoint = track[j] fp.write(" {OLD_%s %d %d}\n" % tuple(waypoint)) fp.write("#enddef\n\n") fp.write("# trackplacer: tracks end\n") fp.write(self.after) fp.close() self.modified = False else: raise IOException("File must have a .trk or .cfg extension.", fp.name) def read(self, fp): "Initialize a journey from map and track information." if type(fp) == type(""): try: fp = open(fp) except IOError: raise IOException("Cannot read file.", fp) if self.tracks: raise IOException("Reading with tracks nonempty.", fp.name) if fp.name.endswith(".png") or fp.name.endswith(".jpg"): self.mapfile = self.properties['map'] = fp.name self.selected_id = "JOURNEY" self.tracks[self.selected_id] = [] return if not fp.name.endswith(".cfg"): raise IOException("Cannot read this filetype.", fp.name) waypoint_re = re.compile("{NEW_(" + "|".join(icon_presentation_order) + ")" \ + " +([0-9]+) +([0-9]+)}") property_re = re.compile("# *trackplacer: ([^=]+)=(.*)") define_re = re.compile("#define ([^_]*)") state = "before" for line in fp: # This is how we ignore stuff outside of track sections if state == "before": if line.startswith("# trackplacer: tracks begin"): state = "tracks" # And fall through... else: self.before += line continue elif state == "after": self.after += line continue elif line.startswith("# trackplacer: tracks end"): state = "after" continue # Which track are we appending to? m = re.search(define_re, line) if m: self.selected_id = m.group(1) if self.selected_id not in self.track_list: self.track_list.append(self.selected_id) self.tracks[self.selected_id] = [] continue # Is this a track marker? m = re.search(waypoint_re, line) if m: try: tag = m.group(1) x = int(m.group(2)) y = int(m.group(3)) self.tracks[self.selected_id].append((tag, x, y)) continue except ValueError: raise IOException("Invalid coordinate field.", fp.name, i+1) # Is it a property setting? m = re.search(property_re, line) if m: self.properties[m.group(1)] = m.group(2) continue if "map" in self.properties: self.mapfile = self.properties['map'] else: raise IOException("Missing map declaration.", fp.name) fp.close() self.modified = False def __getitem__(self, n): return self.tracks[self.selected_id][n] def __setitem__(self, n, v): self.tracks[self.selected_id][n] = v def has_unsaved_changes(self): return self.modified def neighbors(self, x, y): "Return list of neighbors on selected track, enumerated and sorted by distance." neighbors = [] candidates = zip(range(len(self.selected_track())), self.selected_track()) candidates.sort(lambda (i1, (a1, x1, y1)), (i2, (a2, x2, y2)): cmp(distance(x, y, x1, y1), distance(x, y, x2, y2))) return candidates def find(self, x, y): "Find all actions at the given pointin in the selected track." candidates = [] for (i, (tag, xt, yt)) in enumerate(self.selected_track()): if x == xt and y == yt: candidates.append(i) return candidates def insert(self, (action, x, y)): "Insert a feature in the selected track." neighbors = self.neighbors(x, y) # There are two or more markers and we're not nearest the end one if len(neighbors) >= 2 and neighbors[0][0] != len(neighbors)-1: closest = neighbors[0] next_closest = neighbors[1] # If the neighbors are adjacent, insert between them if abs(closest[0] - next_closest[0]) == 1: self.selected_track().insert(max(closest[0], next_closest[0]), (action, x, y)) return # Otherwise, append self.selected_track().append((action, x, y)) self.modified = True def remove(self, x, y): "Remove a feature from the selected track." found = self.find(x, y) if found: # Prefer to delete the most recent feature track = self.selected_track() track = track[:found[-1]] + track[found[-1]+1:] self.modified = True def __str__(self): return self.mapfile + ": " + `self.tracks` class TrackEditorIcon: def __init__(self, action, path): self.action = action # We need an image for the toolbar... self.image = gtk.Image() self.image.set_from_file(path) # ...and a pixbuf for drawing on the map with. self.icon = gtk.gdk.pixbuf_new_from_file(path) self.icon_width = self.icon.get_width() self.icon_height = self.icon.get_height() def bounding_box(self, x, y): "Return a bounding box for this icon when centered at (x, y)." # The +1 is a slop factor allowing for even-sized icons return (x-self.icon_width/2, y-self.icon_height/2, self.icon_width+1, self.icon_height+1) class TrackEditor: def __init__(self, path=None, verbose=False): self.verbose = verbose # Initialize our info about the map and track self.journey = JourneyTracks() self.last_read = None self.journey.read(path) if path.endswith(".cfg"): self.last_read = path self.log("Initial track is %s" % self.journey) self.action = "JOURNEY" self.selected = None self.visible_set = self.journey.track_list[:] # Backing pixmap for drawing area self.pixmap = None # Grab the map into a pixmap self.log("about to read map %s" % self.journey.mapfile) try: self.map = gtk.gdk.pixbuf_new_from_file(self.journey.mapfile) self.map_width = self.map.get_width() self.map_height = self.map.get_height() self.map = self.map.render_pixmap_and_mask()[0] except: self.fatal_error("Error while reading background map %s" % self.journey.mapfile) # Now get the icons we'll need for scribbling on the map with. try: self.selected_dictionary = {} for (action, path) in selected_icon_dictionary.items(): icon = TrackEditorIcon(action, path) self.log("selected %s icon has size %d, %d" % \ (action, icon.icon_width, icon.icon_height)) self.selected_dictionary[action] = icon self.unselected_dictionary = {} for (action, path) in unselected_icon_dictionary.items(): icon = TrackEditorIcon(action, path) self.log("unselected %s icon has size %d, %d" % \ (action, icon.icon_width, icon.icon_height)) self.unselected_dictionary[action] = icon except: self.fatal_error("error while reading icons") # Window-layout time self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_name ("trackplacer") vbox = gtk.VBox(False, 0) self.window.add(vbox) vbox.show() self.window.connect("destroy", lambda w: gtk.main_quit()) tooltips = gtk.Tooltips() # FIXME: make the control box fixed-size controls = gtk.HBox() vbox.add(controls) controls.show() # The radiobutton array on the left radiobox = gtk.HBox() controls.pack_start(radiobox, expand=False, fill=False, padding=0) radiobox.show() # Fake icon-labeled buttons with liberal use of labels... basebutton = None for action in icon_presentation_order: icon = self.selected_dictionary[action] button = gtk.RadioButton(basebutton) bbox = gtk.HBox() button.add(bbox) bbox.add(icon.image) icon.image.show() bbox.show() if not basebutton: button.set_active(True) basebutton = button button.connect("toggled", self.button_callback, icon.action) radiobox.pack_start(button, expand=True, fill=True, padding=0) button.show() tooltips.set_tip(button, "Place %s events." % action.lower()) # The delete button and its label button = gtk.RadioButton(button) delimage = gtk.Image() delimage.set_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_SMALL_TOOLBAR) bbox = gtk.HBox() button.add(bbox) bbox.add(delimage) delimage.show() bbox.show() button.connect("toggled", self.button_callback, "DELETE") radiobox.pack_start(button, expand=True, fill=True, padding=0) button.show() tooltips.set_tip(button, "Remove events.") # The coordinate display in the middle self.coordwin = gtk.Label() controls.pack_start(self.coordwin, expand=True, fill=False, padding=0) self.coordwin.show() # The button array on the right buttonbox = gtk.HBox() controls.pack_end(buttonbox, expand=False, fill=False, padding=0) buttonbox.show() # A quit button button = gtk.Button("Quit") buttonbox.pack_end(button, expand=False, fill=False, padding=10) button.connect_object("clicked", self.quit, self.window) tooltips.set_tip(button, "Leave this program.") button.show() # A help button button = gtk.Button("Help") buttonbox.pack_end(button, expand=False, fill=False, padding=10) button.connect_object("clicked", self.help_handler, self.window) tooltips.set_tip(button, "See a help message describing the controls.") button.show() # A tracks button button = gtk.Button("Tracks") buttonbox.pack_end(button, expand=False, fill=False, padding=10) button.connect_object("clicked", self.tracks_handler, self.window) tooltips.set_tip(button, "Select tracks to be displayed.") button.show() # A properties button button = gtk.Button("Properties") buttonbox.pack_end(button, expand=False, fill=False, padding=10) button.connect_object("clicked", self.properties_handler, self.window) tooltips.set_tip(button, "Set properties of the track.") button.show() # A save button button = gtk.Button("Save") buttonbox.pack_end(button, expand=False, fill=False, padding=10) button.connect_object("clicked", self.save_handler, self.window) tooltips.set_tip(button, "Save track in .cfg format.") button.show() # An animate button button = gtk.Button("Animate") buttonbox.pack_end(button, expand=False, fill=False, padding=10) button.connect_object("clicked", self.animate_handler, self.window) tooltips.set_tip(button, "Animate dot-drawing as in a story part.") button.show() # Create the drawing area on a viewport that scrolls if needed. # Most of the hair here is in trying to query the height # and depth of the widgets surrounding the scrolling area. # The window frame size constants are guesses; they can vary # according to your window manager's policy. They're only used # if the map is too large to fit on the screen. WINDOW_FRAME_WIDTH = 8 WINDOW_FRAME_HEIGHT = 28 screen_width = gtk.gdk.screen_width() screen_height = gtk.gdk.screen_height() self.log("Map size = (%d,%d)" % (self.map_width, self.map_height)) self.log("Screen size = (%d,%d)" % (screen_width, screen_height)) controls_width, controls_height = controls.size_request() self.log("Control box size = (%d,%d)"%(controls_width, controls_height)) x_frame_width = WINDOW_FRAME_WIDTH y_frame_width = WINDOW_FRAME_HEIGHT + controls_height # No, I don't know why the +2 is needed. Black magic.... s_w = min(screen_width -x_frame_width, self.map_width+2) s_h = min(screen_height-y_frame_width, self.map_height+2) self.log("Scroller size = (%s,%s)" % (s_w, s_h)) scroller = gtk.ScrolledWindow() scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) scroller.set_size_request(s_w, s_h) self.drawing_area = gtk.DrawingArea() self.drawing_area.set_size_request(self.map_width, self.map_height) scroller.add_with_viewport(self.drawing_area) vbox.pack_start(scroller, expand=True, fill=True, padding=0) self.drawing_area.show() scroller.show() # Signals used to handle backing pixmap self.drawing_area.connect("expose_event", self.expose_event) self.drawing_area.connect("configure_event", self.configure_event) # Event signals self.drawing_area.connect("motion_notify_event", self.motion_notify_event) self.drawing_area.connect("button_press_event", self.button_press_event) self.drawing_area.connect("leave_notify_event", lambda w, e: self.coordwin.set_text("")) self.drawing_area.set_events(gtk.gdk.EXPOSURE_MASK | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK) self.window.show() gtk.main() self.log("initialization successful") def button_callback(self, widget, data=None): "Radio button callback, changes selected editing action." if widget.get_active(): self.action = data def refresh_map(self, x=0, y=0, xs=-1, ys=-1): "Refresh part of the drawing area with the appropriate map rectangle." if xs == -1: xs = self.map_width - x if ys == -1: ys = self.map_height - y self.log("Refreshing map in (%d, %d, %d, %d, %d, %d}" % (x, y, x, y, xs, ys)) self.pixmap.draw_drawable(self.default_gc, self.map, x, y, x, y, xs, ys) def box(self, (action, x, y)): "Compute the bounding box for an icon of type ACTION at X, Y." # Assumers selected and unselected icons are the same size return self.selected_dictionary[action].bounding_box(x, y) def snap_to(self, x, y): "Snap a location to the nearest feature on the selected track whose bounding box holds it." self.log("Neighbors of %d, %d are %s" % (x, y, self.journey.neighbors(x, y))) for (i, item) in self.journey.neighbors(x, y): if within(x, y, self.box(item)): return i else: return None def neighbors(self, (action, x, y)): "Return all track items with bounding boxes overlapping this one:" rect = self.selected_dictionary[action].bounding_box(x, y) return filter(lambda item: overlaps(rect, self.box(item)), self.journey.selected_track()) def erase_feature(self, widget, (action, x, y)): "Erase specified (active) icon from the map." # Erase all nearby features that might have been damaged. save_select = self.journey.selected_id for (id, track) in self.journey.tracks.items(): self.journey.set_selected_track(id) neighbors = self.neighbors((action, x, y)) for (na, nx, ny) in neighbors: rect = self.box((na, nx, ny)) self.log("Erasing action=%s, dest=%s" % (na, rect)) self.refresh_map(*rect) widget.queue_draw_area(*rect) # Redraw all nearby features except what we're erasing. for (na, nx, ny) in neighbors: if x != nx and y != ny: self.log("Redrawing action=%s" % ((na, nx, ny),)) self.draw_feature(widget, (na, nx, ny)) self.journey.set_selected_track(save_select) def draw_feature(self, widget, (action, x, y), selected): "Draw specified icon on the map." rect = self.box((action, x, y)) self.log("Drawing action=%s (%s), dest=%s" % (action, selected, rect)) if selected: icon = self.selected_dictionary[action].icon else: icon = self.unselected_dictionary[action].icon self.pixmap.draw_pixbuf(self.default_gc, icon, 0, 0, *rect) widget.queue_draw_area(*rect) def redraw(self, widget, delay=0): "Redraw the map and tracks." self.refresh_map() # Draw all unselected tracks before the selected one, # so any icons coinciding will be drawn in the right order. for (id, track) in self.journey.tracks.items(): if id in self.visible_set and id != self.journey.selected_id: for item in self.journey.tracks[id]: self.draw_feature(widget, item, False) if delay: time.sleep(delay) self.expose_event(widget) while gtk.events_pending(): gtk.main_iteration(False) for (id, track) in self.journey.tracks.items(): if id in self.visible_set and id == self.journey.selected_id: for item in self.journey.tracks[id]: self.draw_feature(widget, item, True) if delay: time.sleep(delay) self.expose_event(widget) while gtk.events_pending(): gtk.main_iteration(False) while gtk.events_pending(): gtk.main_iteration(False) def configure_event(self, widget, event): "Create a new backing pixmap of the appropriate size." x, y, width, height = widget.get_allocation() self.pixmap = gtk.gdk.Pixmap(widget.window, width, height) self.default_gc = self.drawing_area.get_style().fg_gc[gtk.STATE_NORMAL] self.redraw(widget) return True def expose_event(self, widget, event=None): "Redraw the screen from the backing pixmap" if event: x , y, width, height = event.area else: x, y, width, height = widget.get_allocation() widget.window.draw_drawable(self.default_gc, self.pixmap, x, y, x, y, width, height) return False def button_press_event(self, widget, event): if event.button == 1 and self.pixmap != None: x = int(event.x) y = int(event.y) self.selected = self.snap_to(x, y) # Skip the redraw in half the cases self.log("Action %s at (%d, %d): feature = %s" % (self.action, x, y, self.selected)) if (self.selected == None) and (self.action == "DELETE"): return if (self.selected != None) and (self.action != "DELETE"): return # Actual drawing and mutation of the journey track happens here if not self.selected and self.action != "DELETE": self.draw_feature(widget, (self.action, x, y), True) self.journey.insert((self.action, x, y)) elif self.selected != None and self.action == "DELETE": (action, x, y) = self.journey[self.selected] self.log("Deletion snapped to feature %d %s" % (self.selected,(action,x,y))) self.erase_feature(widget, (action, x, y)) self.journey.remove(x, y) self.log("Track is %s" % self.journey) return True def motion_notify_event(self, widget, event): if event.is_hint: x, y, state = event.window.get_pointer() else: x = event.x y = event.y self.coordwin.set_text("(%d, %d)" % (x, y)) state = event.state # This code enables dragging icons. if state & gtk.gdk.BUTTON1_MASK and self.pixmap != None: if self.selected is not None: (action, lx, ly) = self.journey[self.selected] self.erase_feature(widget, (action, lx, ly)) self.journey[self.selected] = (action, x, y) self.journey.modified = True self.draw_feature(widget, (action, x, y), True) self.log("Track is %s" % self.journey) return True def quit(self, w): if self.journey.has_unsaved_changes(): self.quit_check = gtk.Dialog(title="Really quit?", parent=None, flags=gtk.DIALOG_MODAL, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label("Track has unsaved changes. OK to quit?") self.quit_check.vbox.pack_start(label) label.show() response = self.quit_check.run() self.quit_check.destroy() if response == gtk.RESPONSE_ACCEPT: sys.exit(0) else: sys.exit(0) def save_handler(self, w): "Save track data," if not self.journey.has_unsaved_changes(): w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=gtk.BUTTONS_OK) w.set_markup("You have no unsaved changes.") w.run() w.destroy() else: # Request save file name dialog = gtk.FileChooserDialog("Save track macros", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_CANCEL) if self.last_read: dialog.set_filename(self.last_read) dialog.set_show_hidden(False) sfilter = gtk.FileFilter() sfilter.set_name("Track files") sfilter.add_pattern("*.cfg") dialog.add_filter(sfilter) response = dialog.run() filename = dialog.get_filename() dialog.destroy() if response == gtk.RESPONSE_CANCEL: return # Relativize file path to current directory if filename.startswith(os.getcwd()): filename = filename[len(os.getcwd())+1:] # Request overwrite confirmation if this is a save-as if os.path.exists(filename) and filename != self.last_read: save_check = gtk.Dialog(title="Really overwrite?", parent=None, flags=gtk.DIALOG_MODAL, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label("Overwrite existing data in %s?" % filename) save_check.vbox.pack_start(label) label.show() response = save_check.run() save_check.destroy() if response == gtk.RESPONSE_REJECT: return # Actual I/O self.log("Writing track data to %s" % filename) try: fp = open(filename, "w") except IOError: raise IOException("Cannot write file.", filename) if not self.journey.mapfile: self.journey.mapfile = filename self.journey.write(fp) def help_handler(self, w): "Display help." w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=gtk.BUTTONS_OK) w.set_markup(gui_help) w.run() w.destroy() def tracks_handler(self, w): "Modify the visible set of tracks." self.visibility = gtk.Dialog(title="Edit track visibility.", buttons=(gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label("Change the visibility of tracks.") self.visibility.vbox.pack_start(label) self.visibility_toggles = {} label.show() label = gtk.Label("(Radiobuttons select a track for editing.)") self.visibility.vbox.pack_start(label) self.visibility_toggles = {} label.show() basebutton = None for (i, track_id) in enumerate(self.journey.track_list): h = gtk.HBox() self.visibility.vbox.add(h) h.show() radiobutton = gtk.RadioButton(basebutton) if basebutton == None: basebutton = radiobutton radiobutton.set_active(track_id == self.journey.selected_id) radiobutton.connect("toggled", self.track_activity_callback, track_id) radiobutton.show() h.add(radiobutton) checkbox = gtk.CheckButton(track_id) checkbox.set_active(track_id in self.visible_set) checkbox.connect("toggled", self.track_visibility_callback,track_id) h.add(checkbox) checkbox.show() self.visibility_toggles[track_id] = checkbox self.visibility.show() self.visibility.connect("response", self.track_visibility_revert) def track_activity_callback(self, w, track_id): if w.get_active(): self.journey.set_selected_track(track_id) self.visibility_toggles[track_id].set_active(True) if track_id not in self.visible_set: self.track_visibility_callback(w, track_id) else: self.redraw(self.drawing_area, delay=0.05) def track_visibility_callback(self, w, track_id): if len(self.visible_set) <= 1 and track_id in self.visible_set: w = gtk.MessageDialog(type=gtk.MESSAGE_INFO, flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=gtk.BUTTONS_OK) w.set_markup("At least one track must remain visible.") self.visibility_toggles[track_id].set_active(True) w.run() w.destroy() return self.log("Toggling visibility of %s" % track_id) if track_id in self.visible_set: self.visible_set.remove(track_id) else: self.visible_set.append(track_id) self.log("Visibility set is now %s" % self.visible_set) if self.journey.selected_id not in self.visible_set: self.journey.set_selected_track(self.visible_set[-1]) self.redraw(self.drawing_area, delay=0.05) def track_visibility_revert(self, w, response_id): "On response or window distruction, restire visibility set." self.visible_set = self.journey.track_list self.redraw(self.drawing_area, delay=0.05) self.visibility.destroy() def properties_handler(self, w): "Display a dialog for editing track properties." w = gtk.Dialog(title="Track properties editor", parent=None, flags=gtk.DIALOG_MODAL, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label("You can enter a key/value pair for a new property on the last line.") label.show() w.vbox.pack_start(label) table = gtk.Table(len(self.journey.properties)+1, 2) table.show() w.vbox.pack_start(table) keys = self.journey.properties.keys() keys.sort() labels = [] entries = [] for (i, key) in enumerate(keys): labels.append(gtk.Label(key)) labels[-1].show() table.attach(labels[-1], 0, 1, i, i+1) entries.append(gtk.Entry()) entries[-1].set_text(self.journey.properties[key]) entries[-1].set_width_chars(50) entries[-1].show() table.attach(entries[-1], 1, 2, i, i+1) new_key = gtk.Entry() new_key.set_width_chars(12) new_key.show() table.attach(new_key, 0, 1, len(keys)+1, len(keys)+2) new_value = gtk.Entry() new_value.set_width_chars(50) table.attach(new_value, 1, 2, len(keys)+1, len(keys)+2) new_value.show() response = w.run() w.destroy() if response == gtk.RESPONSE_ACCEPT: for (label, entry) in zip(labels, entries): self.journey.properties[label.get_text()] = entry.get_text() if new_key.get_text() and new_label.get_text(): self.journey.properties[new_key.get_text()] = new_entry.get_text() def animate_handler(self, w): "Animate dot placing as though on a storyboard." self.refresh_map() self.expose_event(self.drawing_area) self.redraw(self.drawing_area, 0.5) def log(self, msg): "Notify user of error and die." if self.verbose: print >>sys.stderr, "trackplacer:", msg def fatal_error(self, msg): "Notify user of error and die." w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK) w.set_markup(msg) w.run() sys.exit(1) if __name__ == "__main__": (options, arguments) = getopt.getopt(sys.argv[1:], "d:hv?", ['directory=', 'help', 'verbose']) verbose = False top = None for (opt, val) in options: if opt in ('-d', '--directory'): top = val elif opt in ('-?', '-h', '--help'): print __doc__ sys.exit(0) elif opt in ('-v', '--verbose'): verbose=True here = os.getcwd() if top: os.chdir(top) else: wesnoth.wmltools.pop_to_top("trackplacer") if arguments: try: TrackEditor(path=os.path.join(here, arguments[0]), verbose=verbose) except IOException, e: if e.lineno: sys.stderr.write(('"%s", line %d: ' % (e.path, e.lineno)) + e.message + "\n") else: sys.stderr.write(e.path + ": " + e.message + "\n") else: while True: try: dialog = gtk.FileChooserDialog("Open track file", None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) dialog.set_default_response(gtk.RESPONSE_OK) dialog.set_filename(default_map) dialog.set_show_hidden(False) ofilter = gtk.FileFilter() ofilter.set_name("Images and Tracks") ofilter.add_mime_type("image/png") ofilter.add_mime_type("image/jpeg") ofilter.add_mime_type("image/gif") ofilter.add_pattern("*.png") ofilter.add_pattern("*.jpg") ofilter.add_pattern("*.gif") ofilter.add_pattern("*.tif") ofilter.add_pattern("*.xpm") ofilter.add_pattern("*.cfg") dialog.add_filter(ofilter) ofilter = gtk.FileFilter() ofilter.set_name("Images only") ofilter.add_mime_type("image/png") ofilter.add_mime_type("image/jpeg") ofilter.add_mime_type("image/gif") ofilter.add_pattern("*.png") ofilter.add_pattern("*.jpg") ofilter.add_pattern("*.gif") ofilter.add_pattern("*.tif") ofilter.add_pattern("*.xpm") dialog.add_filter(ofilter) ofilter = gtk.FileFilter() ofilter.set_name("Tracks only") ofilter.add_pattern("*.cfg") dialog.add_filter(ofilter) response = dialog.run() if response == gtk.RESPONSE_OK: filename = dialog.get_filename() elif response == gtk.RESPONSE_CANCEL: sys.exit(0) dialog.destroy() # Relativize file path to current directory if filename.startswith(os.getcwd()): filename = filename[len(os.getcwd())+1:] TrackEditor(filename, verbose=verbose) except IOException, e: w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, flags=gtk.DIALOG_DESTROY_WITH_PARENT, buttons=gtk.BUTTONS_OK) if e.lineno: errloc = '"%s", line %d:' % (e.path, e.lineno) # Emacs friendliness sys.stderr.write(errloc + " " + e.message + "\n") else: errloc = e.path + ":" w.set_markup(errloc + "\n\n" + e.message) w.run() w.destroy()