wesnoth/data/tools/trackplacer

388 lines
14 KiB
Plaintext
Raw Normal View History

2008-10-11 03:22:26 +00:00
#!/usr/bin/env python
"""
trackplacer -- map journey track editor.
usage: trackplacesr [-vh?] [filename]
If the filename is not specified, tracplacer will pop up a file selector.
Can be started with a map image, in which case we're editing a new journey.
Can be started with a track file. A track file is a text file interpreted
line-by-line; each line is interpreted as whitespace-separated fields.
The first field of the first line must be FILE, and the second field
of that line must be valid filename. Subsequent lines must have three
fields each: an action tag (JOURNEY, BATTLE, or REST) and two numeric
coordinate fields.
A journey is an object containing a map file name and a (possibly empty)
track. This program exists to visually edit journeys.
The -v option enables verbose logging to standard error.
The -h or -? options display this summary.
"""
import sys, exceptions, getopt
2008-10-11 03:22:26 +00:00
import pygtk
pygtk.require('2.0')
import gtk
import wesnoth.wmltools
# All dependencies on the shape of the data tree live here
imagedir = "data/core/images/"
default_map = imagedir + "maps/wesnoth.png"
icon_dictionary = {
"JOURNEY": imagedir + "misc/new-journey.png",
"BATTLE": imagedir + "misc/new-battle.png",
"REST": imagedir + "misc/flag-red.png",
}
# Size of the rectangle around the mouse pointer within which the code
# will seek tracking dots. Should be about (N-1)/2 where N is the
# pixel radius of the journey dot. For this and other reasons, it's
# helpful id the tracking dot and other icons all have a square
# aspect ratio and an odd number of pixels on the sise, so each has
# a well-defibed center pixel.
vision_distance = 5
class ReadException(exceptions.Exception):
"Exception thrown while reading a track file."
def __init__(self, message, filename, lineno):
self.message = message
self.filename = filename
self.lineno = lineno
class JourneyTrack:
"Represent a journey track on a map."
def __init__(self):
self.filename = None # Map background of the journey
self.track = [] # List of (action, x, y) tuples
def write(self, fp):
"Record a journey track."
fp.write("FILE %s\n" % self.filename)
for location in self.track:
fp.write("%s %d %d\n" % location)
def read(self, fp):
"Initialize a journey from map and track information."
if type(fp) == type(""):
try:
fp = open(fp)
except FileError:
raise ReadException("cannot read", fp)
if self.track:
raise ReadException("reading with track nonempty", fp.name, 1)
if fp.name.endswith(".png") or fp.name.endswith(".jpg"):
self.filename = fp.name
return
if not fp.name.endswith(".trk"):
raise ReadException("cannot read this filetype", fp.name, 0)
header = fp.readline().split()
if header[0] != 'FILE':
raise ReadException("missing FILE element", fp.name, 1)
else:
self.filename = header[1]
while (i, line) in enumerate(fp):
fields = line.split()
if len(fields) != 3:
raise ReadException("ill-formed line", fp.name, i+1)
(tag, x, y) = fields
if tag not in ("JOURNEY", "BATTLE", "REST"):
raise ReadException("invalid tag field", fp.name, i+1)
try:
x = int(x)
y = int(y)
except ValuError:
raise ReadException("invalid coordinate field", fp.name, i+1)
self.track.append((tag, x, y))
def __getitem__(self, n):
return self.track[n]
def __setitem__(self, n, v):
self.track[n] = v
def find(self, x, y, d=vision_distance):
"Find all track actions near a given point."
candidates = []
ind = 0
for (tag, xt, yt) in self.track:
if x >= xt - d and x <= xt + d and y >= yt - d and y <= yt + d:
candidates.append(ind)
ind += 1
return candidates
def append(self, tag, x, y):
"Append a feature to the track."
self.track.append((tag, x, y))
def remove(self, x, y):
"Remove a feature from the track."
ind = self.find(x, y)
print "Deleting ", ind
if ind:
# Prefer to delete the most recent feature
self.track = self.track[:ind[-1]] + self.track[ind[-1]+1:]
def snap_to(self, x, y, d=vision_distance):
"Snap a location to a nearby track feature, if there is one."
candidates = []
for (tag, xt, yt) in self.track:
if x >= xt - d and x <= xt + d and y >= yt - d and y <= yt + d:
candidates.append((xt, yt))
if len(candidates) == 1:
return candidates[0]
else:
return None
def __str__(self):
return self.filename + ": " + `self.track`
2008-10-11 03:22:26 +00:00
class ModalFileSelector:
def __init__(self, default, blocker=False):
2008-10-11 03:22:26 +00:00
self.default = default
self.filename = None
# Create a new file selection widget
self.filew = gtk.FileSelection("File selection")
self.filew.set_modal(True);
2008-10-11 03:22:26 +00:00
if blocker:
self.filew.connect("destroy", lambda w: sys.exit(0))
self.filew.ok_button.connect("clicked", self.selection_ok)
self.filew.cancel_button.connect("clicked", self.selection_canceled)
2008-10-11 03:22:26 +00:00
self.filew.set_filename(self.default)
self.filew.run()
2008-10-11 03:22:26 +00:00
def selection_canceled(self, widget):
self.filename = None
self.filew.destroy()
2008-10-11 03:22:26 +00:00
def selection_ok(self, widget):
self.filename = self.filew.get_filename()
self.filew.destroy()
2008-10-11 03:22:26 +00:00
class TrackEditorIcon:
def __init__(self, action, filename):
self.action = action
self.image = gtk.Image()
self.image.set_from_file(filename)
class TrackEditor:
def __init__(self, filename=None, verbose=False):
self.verbose = verbose
# Initialize our info about the map and track
self.journey = JourneyTrack()
self.journey.read(filename)
self.action = "JOURNEY"
# Backing pixmap for drawing area
self.pixmap = None
# Grab the map into a pixmap
self.log("about to read map %s" % self.journey.filename)
try:
self.map = gtk.gdk.pixbuf_new_from_file(self.journey.filename)
self.map_width = self.map.get_width()
self.map_height = self.map.get_height()
self.map = self.map.render_pixmap_and_mask()[0]
except gtk.Gerror:
self.fatal_error("Error while reading background map %s" % self.journey.filename)
# Now get the icons we'll need for scribbling on the map with.
action_dictionary = {}
try:
for (action, filename) in icon_dictionary.items():
action_dictionary[action] = TrackEditorIcon(action, filename)
except:
self.fatal_error("error while reading icons")
# Window-layout time
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.set_name ("Test Input")
vbox = gtk.VBox(False, 0)
window.add(vbox)
vbox.show()
window.connect("destroy", lambda w: gtk.main_quit())
hbox1 = gtk.HBox()
vbox.add(hbox1)
hbox1.show()
# A radiobutton array
radiobox = gtk.HBox()
hbox1.pack_start(radiobox, expand=False, fill=False, padding=0)
radiobox.show()
# Fake icon-labeled buttons with liberal use of labels...
basebutton = None
for icon in action_dictionary.values():
button = gtk.RadioButton(basebutton)
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()
radiobox.pack_start(icon.image,
expand=False, fill=False, padding=0)
icon.image.show()
spacer = gtk.Label(" ")
radiobox.pack_start(spacer, expand=False, fill=False, padding=0)
spacer.show()
# The delete button and its label
button = gtk.RadioButton(button)
button.connect("toggled", self.button_callback, "DELETE")
radiobox.pack_start(button, expand=True, fill=True, padding=0)
button.show()
delimage = gtk.Image()
delimage.set_from_stock(gtk.STOCK_DELETE, gtk.ICON_SIZE_SMALL_TOOLBAR)
radiobox.pack_start(delimage,
expand=False, fill=False, padding=0)
delimage.show()
spacer = gtk.Label(" ")
radiobox.pack_start(spacer, expand=False, fill=False, padding=0)
spacer.show()
# A quit button
button = gtk.Button("Quit")
hbox1.pack_end(button, expand=False, fill=False, padding=10)
button.connect_object("clicked", lambda w: w.destroy(), window)
button.show()
# A save button
button = gtk.Button("Save")
hbox1.pack_end(button, expand=False, fill=False, padding=10)
button.connect_object("clicked", lambda w: w.destroy(), window)
button.show()
# A help button
button = gtk.Button("Help")
hbox1.pack_end(button, expand=False, fill=False, padding=10)
button.connect_object("clicked", lambda w: w.destroy(), window)
button.show()
# Create the drawing area
self.drawing_area = gtk.DrawingArea()
self.drawing_area.set_size_request(self.map_width, self.map_height)
vbox.pack_start(self.drawing_area, expand=True, fill=True, padding=0)
self.drawing_area.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.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)
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 apprpriate map rectangle."
if xs == -1:
xs = self.map_width - x
if ys == -1:
ys = self.map_height - y
self.pixmap.draw_drawable(self.default_gc, self.map, x, y, x, y, xs, ys)
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.refresh_map()
return True
def expose_event(self, widget, event):
"Redraw the screen from the backing pixmap"
x , y, width, height = event.area
widget.window.draw_drawable(self.default_gc,
self.pixmap, x, y, x, y, width, height)
return False
def bounding_box(self, p, d=vision_distance):
"Return a feature-containing rangle around a pixel."
return (int(p[0]-d), int(p[1]-d), d*2, d*2)
def draw_icon(self, widget, x, y):
"Draw or erase a track dot."
feature = self.journey.snap_to(x, y)
# Skip the redraw in half the cases
self.log("Action %s at (%d, %d): feature = %s" % (self.action, x, y, feature))
if (feature == None) and (self.action == "DELETE"):
return
if (feature != None) and (self.action != "DELETE"):
return
# Actual drawing and mutation of the journey track happens here
if not feature and self.action != "DELETE":
rect = self.bounding_box((x, y))
self.pixmap.draw_rectangle(widget.get_style().black_gc, True,
rect[0], rect[1], rect[2], rect[3])
self.journey.track.append((self.action, int(x), int(y)))
elif feature and self.action == "DELETE":
rect = self.bounding_box(feature)
self.refresh_map(*rect)
self.journey.remove(feature[0], feature[1])
widget.queue_draw_area(rect[0], rect[1], rect[2], rect[3])
self.log("Track is %s" % self.journey)
def button_press_event(self, widget, event):
if event.button == 1 and self.pixmap != None:
self.draw_icon(widget, event.x, event.y)
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
state = event.state
# Lets you draw pixels by dragging, not the effect we want.
#if state & gtk.gdk.BUTTON1_MASK and self.pixmap != None:
# self.draw_icon(widget, x, y)
return True
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)
2008-10-11 03:22:26 +00:00
if __name__ == "__main__":
(options, arguments) = getopt.getopt(sys.argv[1:], "hv?", ['verbose', 'help'])
verbose = False
for (opt, val) in options:
if opt in ('-?', '-h', '--help'):
print __doc__
sys.exit(0)
elif opt in ('-v', '--verbose'):
verbose=True
2008-10-11 03:22:26 +00:00
wesnoth.wmltools.pop_to_top("trackplacer")
if arguments:
TrackEditor(filename=arguments[0], verbose=verbose)
else:
TrackEditor(ModalFileSelector(default_map).filename, verbose=verbose)