mirror of
https://github.com/wesnoth/wesnoth
synced 2024-09-21 16:42:07 +00:00
619 lines
25 KiB
Python
Executable File
619 lines
25 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
trackplacer -- map journey track editor.
|
|
|
|
usage: trackplacer [-vh?] [filename]
|
|
|
|
If the 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'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 MAP, 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 represented in track
|
|
files.
|
|
|
|
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.
|
|
"""
|
|
|
|
gui_help = '''\
|
|
This is trackplacer, an editor for visually editing journey tracks on Battle For Wesnoth maps.
|
|
|
|
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.
|
|
|
|
The Save button pops up a file selector asking you to supply a filename to which the track should be saved. If you specify a file with a .trk extension the data will be saved in track format, which trackplacer can reload or edit. Any other file extension will raise an error.
|
|
|
|
The Quit button will ask for confirmation if you have unsaved changes.
|
|
|
|
The Help button displays this message.
|
|
|
|
Design and implementation by Eric S. Raymond, October 2008.
|
|
'''
|
|
|
|
import sys, os, 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"
|
|
icon_dictionary = {
|
|
"JOURNEY": imagedir + "misc/new-journey.png",
|
|
"BATTLE": imagedir + "misc/new-battle.png",
|
|
"REST": imagedir + "misc/flag-red.png",
|
|
}
|
|
icon_presentation_order = ("JOURNEY", "BATTLE", "REST")
|
|
segmenters = ("BATTLE","REST")
|
|
|
|
# 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 largest icon. For this and other reasons, it's
|
|
# helpful if 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-defined center pixel.
|
|
vision_distance = 12
|
|
|
|
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
|
|
|
|
class JourneyTrack:
|
|
"Represent a journey track on a map."
|
|
def __init__(self):
|
|
self.mapfile = None # Map background of the journey
|
|
self.track = [] # List of (action, x, y) tuples
|
|
self.modifications = 0
|
|
self.initial_track = []
|
|
def write(self, fp):
|
|
"Record a journey track."
|
|
if fp.name.endswith(".trk"):
|
|
fp.write("MAP %s\n" % self.mapfile)
|
|
for location in self.track:
|
|
fp.write("%s %d %d\n" % location)
|
|
fp.close()
|
|
elif fp.name.endswith(".cfg"):
|
|
fp.write("# Automatically generated by trackplacer on %s.\n" % \
|
|
time.ctime(time.time()))
|
|
fp.write("# Don't hand-hack -- edit the associated track file\n")
|
|
fp.write("# and then regenerate this instead.\n\n")
|
|
index_tuples = zip(range(len(self.track)), self.track)
|
|
index_tuples = filter(lambda (i, (a, x, y)): a in segmenters,
|
|
index_tuples)
|
|
endpoints = map(lambda (i, t): i, index_tuples)
|
|
if self.track[-1][0] not in segmenters:
|
|
endpoints.append(len(self.track)-1)
|
|
for (i, e) in enumerate(endpoints):
|
|
fp.write("#define STAGE_%d\n" % (i+1,))
|
|
for j in range(0, e+1):
|
|
age="OLD"
|
|
if i == 0 or j > endpoints[i-1]:
|
|
age = "NEW"
|
|
waypoint = (age,) + tuple(self.track[j])
|
|
fp.write(" {%s_%s %d %d}\n" % waypoint)
|
|
fp.write("#enddef\n\n")
|
|
fp.write("#define STAGE_COMPLETE\n")
|
|
for j in range(len(self.track)):
|
|
waypoint = self.track[j]
|
|
fp.write(" {OLD_%s %d %d}\n" % tuple(waypoint))
|
|
fp.write("#enddef\n\n")
|
|
fp.close()
|
|
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.track:
|
|
raise IOException("Reading with track nonempty.", fp.name)
|
|
if fp.name.endswith(".png") or fp.name.endswith(".jpg"):
|
|
self.mapfile = fp.name
|
|
return
|
|
if not fp.name.endswith(".trk"):
|
|
raise IOException("Cannot read this filetype.", fp.name)
|
|
header = fp.readline().split()
|
|
if header[0] != 'MAP':
|
|
raise IOException("Missing MAP element.", fp.name, 1)
|
|
else:
|
|
self.mapfile = header[1]
|
|
for (i, line) in enumerate(fp):
|
|
fields = line.split()
|
|
if len(fields) != 3:
|
|
raise IOException("Ill-formed track file line.", fp.name, i+1)
|
|
(tag, x, y) = fields
|
|
if tag not in ("JOURNEY", "BATTLE", "REST"):
|
|
raise IOException("Invalid tag field on track file line.", fp.name, i+1)
|
|
try:
|
|
x = int(x)
|
|
y = int(y)
|
|
except ValuError:
|
|
raise IOException("Invalid coordinate field.", fp.name, i+1)
|
|
self.initial_track.append((tag, x, y))
|
|
fp.close()
|
|
self.track = self.initial_track[:]
|
|
def has_unsaved_changes(self):
|
|
return self.track != self.initial_track
|
|
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.mapfile + ": " + `self.track`
|
|
|
|
class ModalFileSelector:
|
|
def __init__(self, default, legend):
|
|
self.default = default
|
|
self.path = None
|
|
# Create a new file selection widget
|
|
self.filew = gtk.FileSelection(legend)
|
|
self.filew.set_modal(True);
|
|
|
|
self.filew.ok_button.connect("clicked", self.selection_ok)
|
|
self.filew.cancel_button.connect("clicked", self.selection_canceled)
|
|
if self.default:
|
|
self.filew.set_filename(self.default)
|
|
self.filew.run()
|
|
|
|
def selection_canceled(self, widget):
|
|
self.path = None
|
|
self.filew.destroy()
|
|
|
|
def selection_ok(self, widget):
|
|
self.path = self.filew.get_filename()
|
|
if self.path.startswith(os.getcwd()):
|
|
self.path = self.path[len(os.getcwd())+1:]
|
|
self.filew.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()
|
|
|
|
class TrackEditor:
|
|
def __init__(self, path=None, verbose=False):
|
|
self.verbose = verbose
|
|
# Initialize our info about the map and track
|
|
self.journey = JourneyTrack()
|
|
self.last_read = None
|
|
self.journey.read(path)
|
|
if path.endswith(".trk"):
|
|
self.last_read = path
|
|
self.log("Initial track is %s" % self.journey)
|
|
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.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.
|
|
self.action_dictionary = {}
|
|
try:
|
|
for (action, path) in icon_dictionary.items():
|
|
icon = TrackEditorIcon(action, path)
|
|
self.log("%s icon has size %d, %d" % \
|
|
(action, icon.icon_width, icon.icon_height))
|
|
self.action_dictionary[action] = icon
|
|
except:
|
|
self.fatal_error("error while reading icons")
|
|
|
|
# Window-layout time
|
|
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
|
window.set_name ("trackplacer")
|
|
|
|
vbox = gtk.VBox(False, 0)
|
|
window.add(vbox)
|
|
vbox.show()
|
|
|
|
window.connect("destroy", lambda w: gtk.main_quit())
|
|
|
|
# FIXME: make thw 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.action_dictionary[action]
|
|
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()
|
|
|
|
# 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.conditional_quit, window)
|
|
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, window)
|
|
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, window)
|
|
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)
|
|
|
|
|
|
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 draw_feature(self, widget, x, y, action, erase=False):
|
|
"Draw specified icon on the map."
|
|
icon = self.action_dictionary[action]
|
|
if erase:
|
|
# The +1 is a slop factor allowing for even-sized icons
|
|
rect = (x-icon.icon_width/2, y-icon.icon_height/2,
|
|
icon.icon_width+1, icon.icon_height+1)
|
|
self.log("Erasing action=%s, dest=%s" % (icon.action, rect))
|
|
self.refresh_map(*rect)
|
|
else:
|
|
rect = (x-icon.icon_width/2, y-icon.icon_height/2,
|
|
icon.icon_width, icon.icon_height)
|
|
self.log("Drawing action=%s, dest=%s" % (icon.action, rect))
|
|
self.pixmap.draw_pixbuf(self.default_gc, icon.icon, 0, 0, *rect)
|
|
widget.queue_draw_area(*rect)
|
|
|
|
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()
|
|
for (action, x, y) in self.journey.track:
|
|
self.draw_feature(widget, x, y, action)
|
|
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 button_press_event(self, widget, event):
|
|
if event.button == 1 and self.pixmap != None:
|
|
x = int(event.x)
|
|
y = int(event.y)
|
|
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":
|
|
self.draw_feature(widget, x, y, self.action)
|
|
self.journey.track.append((self.action, x, y))
|
|
elif feature and self.action == "DELETE":
|
|
candidates = self.journey.find(x, y)
|
|
if len(candidates) == 1:
|
|
# Prefer to delete the most recent candidate
|
|
(action, x, y) = self.journey.track[candidates[-1]]
|
|
self.draw_feature(widget, x, y, action, erase=True)
|
|
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
|
|
|
|
# Lets you draw pixels by dragging, not the effect we want.
|
|
#if state & gtk.gdk.BUTTON1_MASK and self.pixmap != None:
|
|
# self.button_left_click(widget, x, y)
|
|
|
|
return True
|
|
|
|
#
|
|
# These two functions implement a civilized quit confirmation that
|
|
# prompts you if you have unnsaved changes
|
|
#
|
|
def conditional_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()
|
|
self.quit_check.connect("response", self.conditional_quit_handler)
|
|
self.quit_check.run()
|
|
else:
|
|
sys.exit(0)
|
|
def conditional_quit_handler(self, widget, id):
|
|
if id == gtk.RESPONSE_ACCEPT:
|
|
sys.exit(0)
|
|
elif id == gtk.RESPONSE_REJECT:
|
|
self.quit_check.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 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:
|
|
w = ModalFileSelector(default=self.last_read, legend="Save track to file")
|
|
if not w.path.endswith(".trk") and not w.path.endswith(".cfg"):
|
|
raise IOException("File must have a .trk or .cfg extension.", w.path)
|
|
if w.path != self.last_read and os.path.exists(w.path):
|
|
self.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?" % w.path)
|
|
self.save_check.vbox.pack_start(label)
|
|
label.show()
|
|
self.save_check.connect("response",
|
|
self.conditional_save_handler)
|
|
self.save_check.run()
|
|
# After conditional_save handler fires
|
|
if not self.save_confirm:
|
|
return
|
|
self.log("Writing track data to %s" % w.path)
|
|
try:
|
|
fp = open(w.path, "w")
|
|
except IOError:
|
|
raise IOException("Cannot write file.", w.path)
|
|
self.journey.write(fp)
|
|
|
|
def conditional_save_handler(self, widget, id):
|
|
self.save_confirm = (id == gtk.RESPONSE_ACCEPT)
|
|
self.save_check.destroy()
|
|
|
|
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
|
|
|
|
if top:
|
|
os.chdir(top)
|
|
else:
|
|
wesnoth.wmltools.pop_to_top("trackplacer")
|
|
if arguments:
|
|
try:
|
|
TrackEditor(path=arguments[0], verbose=verbose)
|
|
except IOException, e:
|
|
sys.stderr.write(e.message + "\n")
|
|
else:
|
|
while True:
|
|
try:
|
|
selector = ModalFileSelector(default=default_map,
|
|
legend="Track or map file to read")
|
|
if not selector.path:
|
|
break
|
|
TrackEditor(selector.path, 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()
|