
It's mostly about making the scripts run if python defaults to python3. Has been tested for each script.
1275 lines
54 KiB
Python
Executable file
1275 lines
54 KiB
Python
Executable file
#!/usr/bin/env python2
|
|
'''
|
|
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 .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
|
|
select 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 it will look for
|
|
track information enclosed in special comments that look like this:
|
|
|
|
# trackplacer: tracks begin
|
|
# trackplacer: tracks end
|
|
|
|
trackplacer will alter only what it finds inside these comments, except tht it
|
|
will also generate a file epilog for undefining local symbols. The
|
|
epilog will begin with this comment:
|
|
|
|
# trackplacer: epilog begins
|
|
|
|
Special comments may appear in the track section, looking like this:
|
|
|
|
# trackplacer: <property>=<value>
|
|
|
|
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 the editing controls, click the Help button in the trackplacer
|
|
GUI.
|
|
'''
|
|
|
|
gui_help = '''\
|
|
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. 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.
|
|
|
|
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.
|
|
|
|
Click the right button to examine features overlapping the pointer. Each marker on both selected and unselected tracks will be reported.
|
|
|
|
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 animated.
|
|
|
|
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 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.
|
|
|
|
The Help button displays this message.
|
|
|
|
The Quit button ends your session, asking for confirmation if you have unsaved changes.
|
|
'''
|
|
|
|
gui_about = '''\
|
|
This is trackplacer, an editor for visually editing sets of journey tracks on Battle For Wesnoth maps.
|
|
|
|
By Eric S. Raymond for the Battle For Wesnoth project, 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 - 1 and y >= t and y <= t + d - 1:
|
|
return True
|
|
return False
|
|
|
|
def overlaps(p1, p2):
|
|
"Do two rectangles overlap?"
|
|
(x1,y1,x1d,y1d) = p1
|
|
(x2,y2,x2d,y2d) = p2
|
|
return within(x1, y1, p2) or \
|
|
within(x1+x1d, y1, p2) or \
|
|
within(x1, y1+y1d, p2) or \
|
|
within(x1+x1d, y1+y1d, p2) or \
|
|
within(x2, y2, p1) or \
|
|
within(x2+x2d, y2, p1) or \
|
|
within(x2, y2+y2d, p1) or \
|
|
within(x2+x2d, y2+y2d, p1)
|
|
|
|
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_order = []
|
|
self.properties = {}
|
|
self.modified = 0
|
|
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, filename):
|
|
"Record a set of named journey tracks."
|
|
if filename.endswith(".cfg"):
|
|
fp = open(filename, "w")
|
|
fp.write(self.before)
|
|
fp.write("# trackplacer: tracks begin\n#\n")
|
|
fp.write("# Hand-hack this section strictly at your own risk.\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")
|
|
definitions = []
|
|
for name in self.track_order:
|
|
track = self.tracks[name]
|
|
index_tuples = zip(xrange(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)
|
|
outname = name.replace(" ", "_").upper()
|
|
for (i, e) in enumerate(endpoints):
|
|
stagename = "%s_STAGE%d" % (outname, i+1,)
|
|
definitions.append(stagename)
|
|
fp.write("#define %s\n" % stagename)
|
|
for j in xrange(0, e+1):
|
|
age="OLD"
|
|
if i == 0 or j > endpoints[i-1]:
|
|
age = "NEW"
|
|
waypoint = (age,) + tuple(track[j])
|
|
marker = " {%s_%s %d %d}\n" % waypoint
|
|
fp.write(marker)
|
|
fp.write("#enddef\n\n")
|
|
endname = "%s_END" % stagename
|
|
fp.write("#define %s\n" % endname)
|
|
definitions.append(endname)
|
|
for j in xrange(0, e+1):
|
|
age="OLD"
|
|
if j == endpoints[i]:
|
|
age = "NEW"
|
|
waypoint = (age,) + tuple(track[j])
|
|
marker = " {%s_%s %d %d}\n" % waypoint
|
|
fp.write(marker)
|
|
fp.write("#enddef\n\n")
|
|
completename = "%s_COMPLETE" % name
|
|
fp.write("#define %s\n" % completename)
|
|
definitions.append(completename)
|
|
for j in xrange(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.write ("# trackplacer: epilog begins\n\n")
|
|
for name in definitions:
|
|
if "{" + name + "}" not in self.after:
|
|
fp.write("#undef %s\n" % name)
|
|
fp.write ("\n# trackplacer: epilog ends\n")
|
|
fp.close()
|
|
self.modified = 0
|
|
else:
|
|
raise IOException("File must have .cfg extension.", fp.name)
|
|
def read(self, fp):
|
|
"Initialize a journey from map and track information."
|
|
if type(fp) == type(""):
|
|
try:
|
|
fp = open(fp, "rU")
|
|
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.add_track(self.selected_id)
|
|
self.modified = 0
|
|
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 (.*)_STAGE[0-9]+(_END|_COMPLETE)?")
|
|
state = "before"
|
|
ignore = False
|
|
for line in fp:
|
|
if line.startswith("# trackplacer: epilog begins"):
|
|
break
|
|
# 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)
|
|
ignore = m.group(2)
|
|
if self.selected_id not in self.track_order:
|
|
self.track_order.append(self.selected_id)
|
|
self.tracks[self.selected_id] = []
|
|
continue
|
|
# Is this a track marker?
|
|
m = re.search(waypoint_re, line)
|
|
if m and not ignore:
|
|
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 = 0
|
|
def __getitem__(self, n):
|
|
return self.tracks[self.selected_id][n]
|
|
def __setitem__(self, n, v):
|
|
if self.tracks[self.selected_id][n] != v:
|
|
self.modified += 1
|
|
self.tracks[self.selected_id][n] = v
|
|
def add_track(self, name):
|
|
if name not in self.track_order:
|
|
self.tracks[name] = []
|
|
self.track_order.append(name)
|
|
if self.selected_id is None:
|
|
self.selected_id = name
|
|
self.modified += 1
|
|
def remove_track(self, name):
|
|
if name in self.track_order:
|
|
del self.tracks[name]
|
|
self.track_order.remove(name)
|
|
if not self.track_order:
|
|
self.add_track("JOURNEY")
|
|
self.modified += 1
|
|
def rename_track(self, oldname, newname):
|
|
if oldname in self.tracklist and newname not in self.tracklist:
|
|
self.tracks[newname] = self.tracks[oldname]
|
|
self.track_order[self.track_order.index(oldname)] = newname
|
|
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(xrange(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))
|
|
self.modified += 1
|
|
return
|
|
# Otherwise, append
|
|
self.selected_track().append((action, x, y))
|
|
self.modified += 1
|
|
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()
|
|
self.tracks[self.selected_id] = track[:found[-1]] + track[found[-1]+1:]
|
|
self.modified += 1
|
|
def __str__(self):
|
|
rep = self.mapfile + repr(self.track_order) + "\n"
|
|
for name in self.track_order:
|
|
track = self.tracks[name]
|
|
rep += name + ": " + repr(track) + ":\n"
|
|
return rep
|
|
|
|
class ContextPopup:
|
|
def __init__(self, editor):
|
|
self.editor = editor
|
|
self.window = gtk.Window(gtk.WINDOW_POPUP)
|
|
self.window.set_transient_for(None)
|
|
self.window.set_position(gtk.WIN_POS_CENTER_ALWAYS)
|
|
self.window.set_name("trackplacer info")
|
|
self.frame=gtk.Frame()
|
|
self.window.add(self.frame)
|
|
self.frame.show()
|
|
self.vbox = gtk.VBox(False, 0)
|
|
self.frame.add(self.vbox)
|
|
self.vbox.show()
|
|
self.window.show()
|
|
self.position = gtk.Label()
|
|
self.vbox.pack_start(self.position, expand=False, fill=False)
|
|
self.position.show()
|
|
def inform(self, x, y):
|
|
self.position.set_text("At (%d, %d):" % (x, y))
|
|
save_selected = self.editor.journey.selected_id
|
|
local = []
|
|
for name in self.editor.journey.track_order:
|
|
# Gather info
|
|
self.editor.journey.set_selected_track(name)
|
|
for (possible, item) in self.editor.journey.neighbors(x, y):
|
|
if within(x, y, self.editor.box(item)):
|
|
stagecount = 0
|
|
for i in xrange(possible):
|
|
(action, xn, yn) = self.editor.journey[i]
|
|
if action in segmenters:
|
|
stagecount += 1
|
|
local.append((name, possible, self.editor.journey[possible], stagecount))
|
|
self.editor.journey.set_selected_track(save_selected)
|
|
# Display it
|
|
if local:
|
|
for (name, index, (action, x, y), sc) in local:
|
|
legend = "%s at (%d, %d) is %s[%d], stage %d" \
|
|
% (action.capitalize(), x,y, name, index, sc+1)
|
|
label = gtk.Label(legend)
|
|
label.show()
|
|
self.vbox.add(label)
|
|
else:
|
|
label = gtk.Label("No features")
|
|
label.show()
|
|
self.vbox.add(label)
|
|
def destroy(self):
|
|
self.window.destroy()
|
|
|
|
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 TrackController:
|
|
"Object for controlling an individual track in the Tracks dialog."
|
|
def __init__(self, editor, track_id, trackbox, basebutton):
|
|
self.editor = editor
|
|
self.track_id = track_id
|
|
self.hbox = gtk.HBox()
|
|
trackbox.add(self.hbox)
|
|
self.hbox.show()
|
|
self.radiobutton = gtk.RadioButton(basebutton)
|
|
self.radiobutton.set_active(track_id == editor.journey.selected_id)
|
|
self.radiobutton.connect("toggled",
|
|
editor.track_activity_callback, track_id)
|
|
self.radiobutton.set_tooltip_text("Select %s for editing" % track_id)
|
|
self.radiobutton.show()
|
|
self.hbox.add(self.radiobutton)
|
|
self.checkbox = gtk.CheckButton()
|
|
self.checkbox.set_active(track_id in editor.visible_set)
|
|
self.checkbox.connect("toggled",
|
|
editor.track_visibility_callback, track_id)
|
|
self.hbox.add(self.checkbox)
|
|
self.checkbox.set_tooltip_text("Toggle visibility of %s" % track_id)
|
|
self.checkbox.show()
|
|
self.rename = gtk.Entry()
|
|
self.rename.set_text(track_id)
|
|
self.rename.connect("activate", self.track_rename_handler, track_id)
|
|
self.rename.set_tooltip_text("Change name of track %s" % track_id)
|
|
self.rename.show()
|
|
self.hbox.add(self.rename)
|
|
# We really should have been able to do this:
|
|
# self.deleter = gtk.Button(stock=gtk.STOCK_DELETE, label="")
|
|
# Instead, we have to writhe and faint in coils because the
|
|
# stock argument forces the label.
|
|
self.deleter = gtk.Button()
|
|
delimage = gtk.Image()
|
|
delimage.set_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_SMALL_TOOLBAR)
|
|
bbox = gtk.HBox()
|
|
self.deleter.add(bbox)
|
|
bbox.add(delimage)
|
|
delimage.show()
|
|
bbox.show()
|
|
|
|
self.deleter.connect("clicked", self.track_delete_handler, track_id)
|
|
self.hbox.add(self.deleter)
|
|
self.deleter.set_tooltip_text("Delete track %s" % track_id)
|
|
self.deleter.show()
|
|
editor.controller[track_id] = self
|
|
def track_delete_handler(self, w, track_id):
|
|
if track_id in self.editor.visible_set:
|
|
self.editor.visible_set.remove(track_id)
|
|
if track_id == self.editor.journey.selected_id:
|
|
self.editor.track_select(w, self.editor.visible_set[-1])
|
|
# FIXME: This redraw fails when we delete the last track.
|
|
self.editor.redraw(self.editor.drawing_area)
|
|
self.editor.journey.remove_track(track_id)
|
|
self.hbox.hide()
|
|
del self.editor.controller[track_id]
|
|
def track_rename_handler(self, w, track_id):
|
|
editor.journey.rename(track_id, w.get_text())
|
|
self.editor.controller[w.get_text()] = self.editor.controller[track_id]
|
|
del self.editor.controller[track_id]
|
|
|
|
class TracksEditor:
|
|
def __init__(self, path=None, verbose=False, force_save=False):
|
|
self.verbose = verbose
|
|
self.force_save = force_save
|
|
# Initialize our info about the map and track
|
|
self.journey = JourneyTracks()
|
|
self.last_read = None
|
|
self.journey.read(path)
|
|
self.time_last_io = time.time()
|
|
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_order[:]
|
|
self.context_popup = None
|
|
self.pixmap = None # Backing pixmap for drawing area
|
|
|
|
# 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())
|
|
|
|
# Set up toolbar style
|
|
toolbar = gtk.Toolbar()
|
|
toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL)
|
|
toolbar.set_style(gtk.TOOLBAR_BOTH)
|
|
toolbar.set_border_width(1)
|
|
vbox.pack_start(toolbar, expand = False)
|
|
toolbar.show()
|
|
|
|
# Toolbar widget has a fit when we try to pack these separately.
|
|
radiobox1 = gtk.ToolItem()
|
|
radiobox = gtk.HBox()
|
|
radiobox1.add(radiobox)
|
|
radiobox1.show()
|
|
radiobox.show()
|
|
toolbar.insert(radiobox1, -1)
|
|
|
|
# Marker selection
|
|
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, padding=7)
|
|
button.show()
|
|
button.set_tooltip_text("Place %s markers" % 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, padding=7)
|
|
button.show()
|
|
button.set_tooltip_text("Remove markers")
|
|
|
|
# The copy button and its label
|
|
button = gtk.RadioButton(button)
|
|
copyimage = gtk.Image()
|
|
copyimage.set_from_stock(gtk.STOCK_CONVERT, gtk.ICON_SIZE_SMALL_TOOLBAR)
|
|
bbox = gtk.HBox()
|
|
button.add(bbox)
|
|
bbox.add(copyimage)
|
|
copyimage.show()
|
|
bbox.show()
|
|
button.connect("toggled", self.button_callback, "COPY")
|
|
radiobox.pack_start(button, padding=7)
|
|
button.show()
|
|
button.set_tooltip_text("Copy marker from an unselected track")
|
|
|
|
# Sigh - causes elements to jumop around in the toolbar,
|
|
# because when it's not there the application wants the
|
|
# extra space for buttons.
|
|
#self.coordwin = gtk.Label("")
|
|
#coordwrapper = gtk.ToolItem()
|
|
#coordwrapper.add(self.coordwin)
|
|
#toolbar.add(coordwrapper)
|
|
#coordwrapper.set_expand(True)
|
|
#self.coordwin.show()
|
|
#coordwrapper.show()
|
|
|
|
spacer = gtk.SeparatorToolItem()
|
|
toolbar.add(spacer)
|
|
spacer.set_draw(False)
|
|
spacer.set_expand(True)
|
|
spacer.show()
|
|
|
|
quit = gtk.ToolButton(gtk.STOCK_QUIT)
|
|
toolbar.insert(quit, -1)
|
|
quit.set_tooltip_text("Leave this program.")
|
|
quit.connect("clicked", self.quit)
|
|
quit.show()
|
|
|
|
save = gtk.ToolButton(gtk.STOCK_SAVE)
|
|
toolbar.insert(save, -1)
|
|
save.set_tooltip_text("Save journey tracks.")
|
|
save.connect("clicked", self.save_handler)
|
|
save.show()
|
|
|
|
properties = gtk.ToolButton(gtk.STOCK_PROPERTIES)
|
|
toolbar.insert(properties, -1)
|
|
properties.set_tooltip_text("St properties of the tracks.")
|
|
properties.connect("clicked", self.properties_handler)
|
|
properties.show()
|
|
|
|
animate = gtk.ToolButton(gtk.STOCK_REFRESH)
|
|
animate.set_label(label="Animate")
|
|
toolbar.insert(animate, -1)
|
|
animate.set_tooltip_text("Animate tracks as in story parts.")
|
|
animate.connect("clicked", self.animate_handler)
|
|
animate.show()
|
|
|
|
tracks = gtk.ToolButton(gtk.STOCK_INDEX)
|
|
tracks.set_label(label="Tracks")
|
|
toolbar.insert(tracks, -1)
|
|
tracks.set_tooltip_text("Add, edit, delete and rename tracks.")
|
|
tracks.connect("clicked", self.tracks_handler)
|
|
tracks.show()
|
|
|
|
help = gtk.ToolButton(gtk.STOCK_HELP)
|
|
toolbar.insert(help, -1)
|
|
help.set_tooltip_text("Get command help for this program.")
|
|
help.connect("clicked", self.help_handler)
|
|
help.show()
|
|
|
|
about = gtk.ToolButton(gtk.STOCK_ABOUT)
|
|
toolbar.insert(about, -1)
|
|
about.set_tooltip_text("See credits for this program.")
|
|
about.connect("clicked", self.about_handler)
|
|
about.show()
|
|
|
|
# Create the drawing area on a viewport that scrolls, if needed.
|
|
self.drawing_area = gtk.DrawingArea()
|
|
self.drawing_area.set_size_request(self.map_width, self.map_height)
|
|
screen_width = gtk.gdk.screen_width()
|
|
screen_height = gtk.gdk.screen_height()
|
|
if self.map_width < 0.75 * screen_width and self.map_height < 0.75 * screen_width:
|
|
# Screen is large relative to the image. Grab enough
|
|
# space to display the entire map. and never scroll.
|
|
# There should be enough space around the edges for window
|
|
# decorations, task bars, etc.
|
|
vbox.pack_start(self.drawing_area, expand=True, fill=True, padding=0)
|
|
self.drawing_area.show()
|
|
else:
|
|
# Screen is small. Grab all the space the window manager will
|
|
# give us and deal with scrolling.
|
|
scroller = gtk.ScrolledWindow()
|
|
scroller.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
|
scroller.add_with_viewport(self.drawing_area)
|
|
vbox.pack_start(scroller)
|
|
self.window.maximize()
|
|
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("button_release_event", self.button_release_event)
|
|
|
|
self.drawing_area.connect("leave_notify_event", self.leave_area_event)
|
|
|
|
self.drawing_area.set_events(gtk.gdk.EXPOSURE_MASK
|
|
| gtk.gdk.LEAVE_NOTIFY_MASK
|
|
| gtk.gdk.BUTTON_PRESS_MASK
|
|
| gtk.gdk.BUTTON_RELEASE_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."
|
|
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."
|
|
# Assumes 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():
|
|
if id not in self.visible_set:
|
|
continue
|
|
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),
|
|
save_select == self.journey.selected_id)
|
|
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 flush(self, widget):
|
|
"Force pending events out."
|
|
self.expose_event(widget)
|
|
while gtk.events_pending():
|
|
gtk.main_iteration(False)
|
|
|
|
def redraw(self, widget, delay=0):
|
|
"Redraw the map and tracks."
|
|
self.refresh_map()
|
|
for track_id in self.journey.track_order:
|
|
if track_id not in self.visible_set:
|
|
continue
|
|
for item in self.journey.tracks[track_id]:
|
|
self.draw_feature(widget, item, track_id == self.journey.selected_id)
|
|
if delay:
|
|
time.sleep(delay)
|
|
self.flush(widget)
|
|
# To ensure items on selected track are on top, redraw them
|
|
if self.journey.track_order:
|
|
for item in self.journey.selected_track():
|
|
self.draw_feature(widget, item, True)
|
|
self.flush(widget)
|
|
|
|
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 self.pixmap is None:
|
|
return
|
|
if self.journey.selected_track() is None:
|
|
w = gtk.MessageDialog(type=gtk.MESSAGE_ERROR, buttons=gtk.BUTTONS_OK)
|
|
w.set_markup("No track to edit!")
|
|
w.run()
|
|
return
|
|
# Pick up state information whatever button is pressed
|
|
a = self.action
|
|
x = int(event.x)
|
|
y = int(event.y)
|
|
self.selected = self.snap_to(x, y)
|
|
# Event button 1 - draw
|
|
if event.button == 1:
|
|
# 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 == "COPY":
|
|
save_selected = self.journey.selected_id
|
|
most_recent = None
|
|
for name in self.journey.track_order:
|
|
if name != save_selected:
|
|
self.journey.set_selected_track(name)
|
|
possible = self.snap_to(x, y)
|
|
if possible is not None:
|
|
print "Found possible on", name
|
|
most_recent = (name, possible, self.journey[possible])
|
|
self.journey.set_selected_track(save_selected)
|
|
if most_recent:
|
|
(nn, np, (an, xn, yn)) = most_recent
|
|
self.log("Copy feature: %s[%d] = %s" % (nn, np, (an,xn,yn)))
|
|
(a, x, y) = (an, xn, yn)
|
|
else:
|
|
return
|
|
if (self.selected == None) and (a == "DELETE"):
|
|
return
|
|
if (self.selected != None) and (a != "DELETE"):
|
|
return
|
|
# Actual drawing and mutation of the journey track happens here
|
|
if not self.selected and a != "DELETE":
|
|
self.draw_feature(widget, (a, x, y), True)
|
|
self.journey.insert((a, x, y))
|
|
elif self.selected != None and a == "DELETE":
|
|
(a, x, y) = self.journey[self.selected]
|
|
self.log("Deletion snapped to feature %d %s" % (self.selected,(a,x,y)))
|
|
self.erase_feature(widget, (a, x, y))
|
|
self.journey.remove(x, y)
|
|
self.log("Tracks are %s" % self.journey)
|
|
# Event button 3 - query
|
|
if event.button == 3:
|
|
self.context_popup = ContextPopup(self)
|
|
self.context_popup.inform(x, y)
|
|
return True
|
|
|
|
def button_release_event(self, widget, event):
|
|
if self.context_popup is not None:
|
|
self.context_popup.destroy()
|
|
|
|
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 wit h the middle button.
|
|
if state & gtk.gdk.BUTTON2_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 += 1
|
|
self.draw_feature(widget, (action, x, y), True)
|
|
self.log("Tracks are %s" % self.journey)
|
|
return True
|
|
|
|
def leave_area_event(self, w, e):
|
|
if self.context_popup:
|
|
self.context_popup.destroy()
|
|
#self.coordwin.set_text("")
|
|
|
|
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() and not self.force_save:
|
|
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() + os.sep):
|
|
filename = filename[len(os.getcwd())+1:]
|
|
|
|
# Request overwrite confirmation in some circumstances
|
|
confirmation_required = None
|
|
if os.path.exists(filename):
|
|
if filename != self.last_read:
|
|
confirmation_required = "You have requested saving "\
|
|
"to a file other than %s, " \
|
|
"and that file already exists." \
|
|
% self.last_read
|
|
elif os.stat(filename).st_mtime > self.time_last_io:
|
|
confirmation_required = "File has changed "\
|
|
"since last read or written."
|
|
if confirmation_required:
|
|
confirmation_required += "\nReally overwrite %s?" % filename
|
|
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(confirmation_required)
|
|
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:
|
|
self.journey.write(filename)
|
|
if not self.journey.mapfile:
|
|
self.journey.mapfile = filename
|
|
self.time_last_io = time.time()
|
|
except IOError:
|
|
w = gtk.MessageDialog(type=gtk.MESSAGE_INFO,
|
|
flags=gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
buttons=gtk.BUTTONS_OK)
|
|
w.set_markup("Cannot write" + filename)
|
|
w.run()
|
|
w.destroy()
|
|
|
|
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 about_handler(self, w):
|
|
"Display about information."
|
|
w = gtk.MessageDialog(type=gtk.MESSAGE_INFO,
|
|
flags=gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
buttons=gtk.BUTTONS_OK)
|
|
w.set_markup(gui_about)
|
|
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_CLOSE,
|
|
gtk.RESPONSE_ACCEPT))
|
|
label = gtk.Label("The radiobuttons select a track for editing.")
|
|
self.visibility.vbox.pack_start(label)
|
|
self.visibility_toggles = {}
|
|
label.show()
|
|
label = gtk.Label("The checkbuttons toggle the visibility of tracks.")
|
|
self.visibility.vbox.pack_start(label)
|
|
label.show()
|
|
self.controller = {}
|
|
basebutton = None
|
|
self.trackbox = gtk.VBox()
|
|
self.visibility.vbox.add(self.trackbox)
|
|
self.trackbox.show()
|
|
basebutton = gtk.RadioButton() # Dummy, don't show it.
|
|
for (i, track_id) in enumerate(self.journey.track_order):
|
|
TrackController(self, track_id, self.trackbox, basebutton)
|
|
movebox = gtk.HBox()
|
|
label = gtk.Label("The up and down buttons change the track order.")
|
|
self.visibility.vbox.pack_start(label)
|
|
label.show()
|
|
upbutton = gtk.Button(stock=gtk.STOCK_GO_UP)
|
|
movebox.pack_start(upbutton, expand=True, fill=True)
|
|
upbutton.connect("clicked", lambda w: self.track_move(backward=True))
|
|
upbutton.show()
|
|
downbutton = gtk.Button(stock=gtk.STOCK_GO_DOWN)
|
|
movebox.pack_start(downbutton, expand=True, fill=True)
|
|
downbutton.connect("clicked", lambda w: self.track_move(backward=False))
|
|
downbutton.show()
|
|
movebox.show()
|
|
addbox = gtk.HBox()
|
|
addbox.show()
|
|
addlabel = gtk.Label("Add New Track:")
|
|
addlabel.show()
|
|
addbox.add(addlabel)
|
|
addentry = gtk.Entry()
|
|
addentry.show()
|
|
addbox.add(addentry)
|
|
addentry.connect("activate", self.track_add_callback, basebutton)
|
|
self.visibility.vbox.add(movebox)
|
|
self.visibility.vbox.add(addbox)
|
|
self.visibility.connect("response", self.track_visibility_revert)
|
|
self.visibility.show()
|
|
def track_move(self, backward):
|
|
where = self.journey.track_order.index(self.journey.selected_id)
|
|
if backward:
|
|
where += (len(self.journey.track_order) - 1)
|
|
else:
|
|
where += 1
|
|
where %= len(self.journey.track_order)
|
|
self.journey.track_order.remove(self.journey.selected_id)
|
|
self.journey.track_order.insert(where, self.journey.selected_id)
|
|
self.trackbox.reorder_child(self.controller[self.journey.selected_id].hbox, where)
|
|
def track_select(self, w, track_id):
|
|
"Make the specified track the selected one for editing."
|
|
self.journey.set_selected_track(track_id)
|
|
self.controller[track_id].checkbox.set_active(True)
|
|
self.controller[track_id].radiobutton.set_active(True)
|
|
if track_id not in self.visible_set:
|
|
self.track_visibility_callback(self.controller[track_id], track_id)
|
|
else:
|
|
self.redraw(self.drawing_area)
|
|
def track_activity_callback(self, w, track_id):
|
|
"Called (twice) when a track activity radiobutton is toggled."
|
|
if w.get_active():
|
|
self.track_select(w, track_id)
|
|
def track_visibility_callback(self, w, track_id):
|
|
"Called when a track visibility checkbutton is toggled."
|
|
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.controller[track_id].checkbox.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.controller[track_id].radiobutton.set_active(False)
|
|
self.journey.set_selected_track(self.visible_set[-1])
|
|
self.controller[self.visible_set[-1]].radiobutton.set_active(True)
|
|
else:
|
|
self.redraw(self.drawing_area)
|
|
def track_add_callback(self, w, basebutton):
|
|
"Add a new track, and the controller for it, and select it."
|
|
track_id = w.get_text()
|
|
w.set_text("")
|
|
TrackController(self, track_id, self.trackbox, basebutton)
|
|
self.journey.add_track(track_id)
|
|
self.track_select(w, track_id)
|
|
def track_visibility_revert(self, w, response_id):
|
|
"On response or window distruction, restore visibility set."
|
|
self.visible_set = self.journey.track_order
|
|
self.redraw(self.drawing_area)
|
|
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):
|
|
"Debugging report."
|
|
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:fhv?",
|
|
['directory=', 'force', 'help', 'verbose'])
|
|
verbose = force_save = False
|
|
top = None
|
|
for (opt, val) in options:
|
|
if opt in ('-d', '--directory'):
|
|
top = val
|
|
elif opt in ('-f', '--force'):
|
|
force_save = True
|
|
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:
|
|
filename = os.path.join(here, arguments[0])
|
|
# Relativize file path to current directory
|
|
if filename.startswith(os.getcwd() + os.sep):
|
|
filename = filename[len(os.getcwd())+1:]
|
|
TracksEditor(path=filename, verbose=verbose, force_save=force_save)
|
|
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() + os.sep):
|
|
filename = filename[len(os.getcwd())+1:]
|
|
|
|
TracksEditor(filename, verbose=verbose, force_save=force_save)
|
|
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()
|