wesnoth/data/tools/trackviewer.pyw
Steve Cotton 2c9caab3ce Add trackviewer, remove trackplacer
Trackviewer shows a journey without needing to start Wesnoth and refresh the
cache. Combined with tmx_trackplacer, the python3 versions now have
feature-parity with the old trackplacer, without the reliance on python2 or
pygtk (fixes #4365).

At least on Linux you can have both Tiled and this open on the same file. Save
the file in Tiled, alt+tab to this, and press enter to reload the file.
2019-11-18 16:57:26 +01:00

233 lines
8.5 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# encoding: utf-8
"""
%(prog)s example.cfg
%(prog)s example.tmx --track JOURNEY_PART1
Map journey track animation preview tool, shows the journey without needing to
start Wesnoth and refresh the cache. Currently this does not have editing
functions, instead its a support program which assumes youre either editing
the .cfg file with a text editor or using Tiled with tmx_trackplacer.
At least on Linux you can have both Tiled and this open on the same file. Save
the file in Tiled, alt+tab to this, and press enter to reload the file.
"""
# Gtk version by Eric S. Raymond for the Battle For Wesnoth project, October 2008.
# Tkinter version by Steve Cotton for the Battle For Wesnoth project, 2019.
import wesnoth.trackplacer3 as tp3
import argparse
import tkinter
import tkinter.ttk as ttk
import PIL.Image
import PIL.ImageTk
def waypoint_generator(journey):
for track in journey.tracks:
for point in track.waypoints:
yield (track, point)
class ReloadFactory:
"""Encapsulates both the file-handling and any command line options that affect
which tracks will be shown.
"""
def __init__(self):
raise NotImplementedError()
def load_waypoints(self):
"""Parsing the most recent version of the file, return
an iterator for the waypoints that should be drawn.
"""
raise NotImplementedError()
class MapAndWaypointCanvas:
def __init__(self, window, journey, reload_factory, wesnoth_data_dir):
self.window = window
self.journey = journey
self.reload_factory = reload_factory
self.waypoint_generator = None
self.animation_loop_id = None
self.latest_drawn = [tkinter.StringVar(), tkinter.StringVar(), tkinter.StringVar()]
try:
mapimage = PIL.Image.open(self.journey.mapfile)
except:
try:
mapimage = PIL.Image.open(wesnoth_data_dir + "/../" + self.journey.mapfile)
except:
raise Exception("Cant open map image")
background_width, background_height = mapimage.size
self.canvas = tkinter.Canvas(self.window, scrollregion=(0, 0, background_width, background_height))
self.mapphotoimage = PIL.ImageTk.PhotoImage(mapimage)
self.canvas.create_image((0, 0), anchor="nw", image=self.mapphotoimage)
# hope this fits on screen
self.canvas.configure(width=background_width, height=background_height)
self.action_image = {}
for action in tp3.datatypes.selected_icon_dictionary:
with PIL.Image.open(wesnoth_data_dir + "/" + tp3.datatypes.selected_icon_dictionary[action]) as image:
self.action_image[action] = PIL.ImageTk.PhotoImage(image)
# start with all waypoints visible
for (track, point) in reload_factory.load_waypoints():
self.draw_waypoint(point)
def get_widget(self):
return self.canvas
def get_lastest_drawn(self):
"""Returns an array of StringVars which will be updated with the details
of the most recently drawn point."""
return self.latest_drawn
def draw_waypoint(self, point):
if point.action in self.action_image:
self.canvas.create_image((point.x, point.y), image=self.action_image[point.action], tags="waypoint")
else:
self.canvas.create_text((point.x, point.y), text=point.action, fill="red", tags="waypoint")
def clear_all_drawn_waypoints(self):
self.canvas.delete("waypoint")
def _next_frame(self):
try:
(track, point) = next(self.waypoint_generator)
self.draw_waypoint(point)
self.latest_drawn[0].set(track.name)
self.latest_drawn[1].set("{x}, {y}".format(x=point.x, y=point.y))
self.latest_drawn[2].set(point.action)
self.animation_loop_id = self.window.after(500, self._next_frame)
except StopIteration:
self.animation_loop_id = None
pass
def toggle_pause(self):
if self.animation_loop_id:
self.window.after_cancel(self.animation_loop_id)
self.animation_loop_id = None
elif self.waypoint_generator is not None:
self.animation_loop_id = self.window.after(0, self._next_frame)
def restart_animation(self):
"""This rereads the file each time, in case the file has been edited."""
if self.animation_loop_id:
self.window.after_cancel(self.animation_loop_id)
self.animation_loop_id = None
self.clear_all_drawn_waypoints()
self.waypoint_generator = self.reload_factory.load_waypoints()
self.animation_loop_id = self.window.after(0, self._next_frame)
class Controls:
def __init__(self, window, mapAndWaypointCanvas):
self.window = window
self.canvas = mapAndWaypointCanvas
self.frame = tkinter.Frame(self.window)
self.window.bind("<Escape>", self.quit)
self.window.bind("<space>", self.toggle_pause)
self.window.bind("<Return>", self.restart_animation)
self.window.bind("<KP_Enter>", self.restart_animation)
button = ttk.Button(self.frame, takefocus=False, text="Quit\n(escape)", command=self.quit)
button.pack()
button = ttk.Button(self.frame, takefocus=False, text="Pause\n(space)", command=self.toggle_pause)
button.pack()
button = ttk.Button(self.frame, takefocus=False, text="Reload\n(enter)", command=self.restart_animation)
button.pack()
label = ttk.Label(self.frame, text="\n".join([
"This tool only",
"previews the",
"animation, it does",
"not have editing",
"capabilities",
"",
"To change the data,",
"either edit the",
".cfg file or use",
"tmx_trackplacer."]))
label.pack()
for x in self.canvas.get_lastest_drawn():
label = ttk.Label(self.frame, textvariable=x)
label.pack()
def get_widget(self):
return self.frame
def _debug_log_event(self, event):
print(repr(event))
def quit(self, event=None):
self.window.quit()
def toggle_pause(self, event=None):
self.canvas.toggle_pause()
def restart_animation(self, event=None):
self.canvas.restart_animation()
class LoaderImpl(ReloadFactory):
def __init__(self, options):
self.options = options
if options.file is None:
raise RuntimeError("Need a filename to read from")
if options.file.endswith(".cfg"):
self.reader = tp3.CfgFileFormat()
elif options.file.endswith(".tmx"):
self.reader = tp3.TmxFileFormat(wesnoth_data_dir=options.data_dir)
else:
raise RuntimeError("Dont know how to handle input from this file type")
def load_journey(self):
(journey, metadata) = self.reader.read(self.options.file)
return journey
def load_waypoints(self):
journey = self.load_journey()
if options.track is not None:
found_track = None
for track in journey.tracks:
if track.name == options.track:
found_track = track
if found_track is None:
raise RuntimeError("Named track not found in journey")
journey.tracks = [found_track]
return waypoint_generator(journey)
if __name__ == "__main__":
ap = argparse.ArgumentParser(usage=__doc__)
ap.add_argument("file", metavar="filename", help="Read input from this file")
ap.add_argument("--data-dir", metavar="dir",
help='Same as Wesnoths “--data-dir” argument')
ap.add_argument("--track", metavar="track_name",
help='Only show the animation for the named track')
options = ap.parse_args()
if options.data_dir is None:
import os, sys
APP_DIR,APP_NAME=os.path.split(os.path.realpath(sys.argv[0]))
WESNOTH_ROOT_DIR=os.sep.join(APP_DIR.split(os.sep)[:-2]) # pop out "data" and "tools"
options.data_dir=os.path.join(WESNOTH_ROOT_DIR,"data")
reload_factory = LoaderImpl(options)
journey = reload_factory.load_journey()
print("Read data:", str(journey))
window = tkinter.Tk()
window.title("trackplacer animation preview")
canvas = MapAndWaypointCanvas(window, journey, reload_factory, options.data_dir)
controls = Controls(window, canvas)
controls.get_widget().pack(side=tkinter.LEFT)
canvas.get_widget().pack(side=tkinter.LEFT)
window.mainloop()