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