diff --git a/AutoCompleter.py b/AutoCompleter.py new file mode 100755 index 0000000..0351f75 --- /dev/null +++ b/AutoCompleter.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python2 + +from Tkinter import * +import threading +import Queue +from time import sleep +import json +import os +import requests +from PlaceHolder import PlaceHolder + +class AutoCompleter(Entry, PlaceHolder): + def __init__(self, parent, placeholder, **kw): + Entry.__init__(self, parent, **kw) + self.var = self["textvariable"] = StringVar() + self.var.trace('w', self.changed) + + self.parent = parent + + self.lb = Listbox(self.parent, **kw) + self.lb_up = False + self.has_selected = False + self.queue = Queue.Queue() + + PlaceHolder.__init__(self, placeholder) + + self.bind("", self.keypressed) + self.bind('', self.select_all) + self.lb.bind("", self.selection) + + self.update_me() + + def keypressed(self, event): + key=event.keysym + if key == 'Down': + self.down() + elif key == 'Up': + self.up() + elif key in ['Return', 'Right']: + if self.lb_up: + self.selection() + elif key == 'Escape' and self.lb_up: + self.hide_list() + + def select_all(self, event): + event.widget.event_generate('<>') + + def changed(self, name, index, mode): + if self.var.get().__len__() < 3 and self.lb_up or self.has_selected: + self.hide_list() + self.has_selected = False + else: + t = threading.Thread(target=self.query_systems) + t.start() + + def selection(self, event=None): + if self.lb_up: + self.has_selected = True + self.var.set(self.lb.get(ACTIVE)) + self.hide_list() + self.icursor(END) + + def up(self): + if self.lb_up: + if self.lb.curselection() == (): + index = '0' + else: + index = self.lb.curselection()[0] + if index != '0': + self.lb.selection_clear(first=index) + index = str(int(index)-1) + self.lb.selection_set(first=index) + self.lb.activate(index) + + def down(self): + if self.lb_up: + if self.lb.curselection() == (): + index = '0' + else: + index = self.lb.curselection()[0] + if int(index+1) != END: + self.lb.selection_clear(first=index) + index = str(int(index+1)) + + self.lb.selection_set(first=index) + self.lb.activate(index) + else: + self.query_systems() + + def show_results(self, results): + if results: + self.lb.delete(0, END) + for w in results: + self.lb.insert(END,w) + + self.show_list(len(results)) + else: + if self.lb_up: + self.hide_list() + + def show_list(self, height): + self.lb["height"] = height + if not self.lb_up: + self.lb.grid(row=self.grid_info()["row"]+1, columnspan=2) + self.lb_up = True + + def hide_list(self): + if self.lb_up: + self.lb.grid_remove() + self.lb_up = False + + def query_systems(self): + inp = self.var.get() + if inp != self.placeholder and inp.__len__() >= 3: + url = "https://spansh.co.uk/api/systems?" + results = requests.get(url, + params={'q': inp}, + headers={'User-Agent': "EDMC_SpanshRouter 1.0"}) + + lista = json.loads(results.content) + if lista: + self.write(lista) + + def write(self, lista): + self.queue.put(lista) + + def clear(self): + self.queue.put(None) + + def update_me(self): + try: + while 1: + lista = self.queue.get_nowait() + self.show_results(lista) + self.update_idletasks() + except Queue.Empty: + pass + self.after(100, self.update_me) + +if __name__ == '__main__': + root = Tk() + + widget = AutoCompleter(root, "Test") + widget.grid(row=0) + root.mainloop() \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d7e74f..2e61208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file. +## 2.0.0 + +- You can now plot your route directly from EDMC +- A few bugs were fixed + ## 1.2.1 - Added update button which opens the releases page diff --git a/PlaceHolder.py b/PlaceHolder.py new file mode 100644 index 0000000..41c7061 --- /dev/null +++ b/PlaceHolder.py @@ -0,0 +1,31 @@ +from config import config +from Tkinter import END + +class PlaceHolder(): + def __init__(self, placeholder, **kw): + self.placeholder = placeholder + self.placeholder_color = "grey" + self.default_fg_color = config.get('dark_text') + + self.bind("", self.foc_in) + self.bind("", self.foc_out) + + self.put_placeholder() + + def put_placeholder(self): + self['fg'] = self.placeholder_color + if self.get() != self.placeholder: + self.delete(0, END) + self.insert(0, self.placeholder) + + def force_placeholder_color(self): + self['fg'] = self.placeholder_color + + def foc_in(self, *args): + if self['fg'] == self.placeholder_color or self.get() == self.placeholder: + self.delete('0', 'end') + self['fg'] = self.default_fg_color + + def foc_out(self, *args): + if not self.get(): + self.put_placeholder() diff --git a/PlaceHolderEntry.py b/PlaceHolderEntry.py new file mode 100644 index 0000000..fb58798 --- /dev/null +++ b/PlaceHolderEntry.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python2 + +from Tkinter import * +from PlaceHolder import PlaceHolder + +class PlaceHolderEntry(Entry, PlaceHolder): + def __init__(self, parent, placeholder, **kw): + Entry.__init__(self, parent, **kw) + PlaceHolder.__init__(self, placeholder) \ No newline at end of file diff --git a/README.md b/README.md index 2f0d23a..fb1cf26 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ # EDMC_SpanshRouter -This plugin's purpose is to automatically copy to your clipboard the next waypoint on a route you planned using [Spansh](https://www.spansh.co.uk/plotter). +This plugin's purpose is to automatically copy to your clipboard the next waypoint on a route you planned using [Spansh](https://www.spansh.co.uk/plotter). ## Install - Download the latest release [here](https://github.com/CMDR-Kiel42/EDMC_SpanshRouter/releases) and unzip it. - Open your EDMC plugins folder - in EDMC settings, select "Plugins" tab, click the "Open" button. - Create a folder inside the plugins folder called **SpanshRouter** -- Open the **SpanshRouter** folder and put **load.py** inside. +- Open the **SpanshRouter** folder and put all the files you extracted inside. - Restart EDMC - ## How to use -- Once you've plotted your route on [Spansh](https://www.spansh.co.uk/plotter), download the CSV file that it generates -- On EDMC, click the **Upload new route** button and choose your file -- The next waypoint is now copied into your clipboard! Simply paste it into your Galaxy Map, and *voilĂ *! +You can either plot your route directly from EDMC, or you can import a CSV file from [Spansh](https://www.spansh.co.uk/plotter) -Once you reach your next waypoint, the plugin will automatically update your clipboard, so you just need to go to your Galaxy Map and paste it everytime you reach a waypoint. +Once your route is plotted, and every time you reach a waypoint, the next one is automatically copied to your clipboard. + +You just need to go to your Galaxy Map and paste it everytime you reach a waypoint. If for some reason, your clipboard should be empty or containing other stuff that you copied yourself, just click on the **Next waypoint** button, and the waypoint will be copied again to your clipboard. @@ -25,10 +24,11 @@ If you close EDMC, the plugin will save your progress. The next time you run EDM Fly dangerous! o7 - ## Known Issues + At the moment, plotting a route while the game is running, and which begins from the system you're currently in, doesn't update your "next waypoint". If you're in that situation, a couple of solutions are available: -* Performing a FSS Discovery Scan -* Go in our out of Supercruise -* Logging back in and out while EDMC is already running. +- Using the **down** button below the **Next waypoint** +- Performing a FSS Discovery Scan +- Go in our out of Supercruise +- Logging back in and out while EDMC is already running. diff --git a/load.py b/load.py index a55696b..a013963 100644 --- a/load.py +++ b/load.py @@ -1,21 +1,22 @@ import Tkinter as tk import tkFileDialog as filedialog import tkMessageBox as confirmDialog -from ttkHyperlinkLabel import HyperlinkLabel import sys import csv import os -from monitor import monitor import urllib import json import webbrowser +import requests +from AutoCompleter import AutoCompleter +from PlaceHolderEntry import PlaceHolderEntry if sys.platform.startswith('linux'): import subprocess this = sys.modules[__name__] -this.plugin_version = "1.2.1" +this.plugin_version = "2.0.0" this.update_available = False this.next_stop = "No route planned" this.route = [] @@ -81,8 +82,8 @@ def plugin_stop(): print("No route to delete") -def update_gui(): - if not this.route.__len__() > 0: +def show_route_gui(show): + if not show or not this.route.__len__() > 0: this.waypoint_prev_btn.grid_remove() this.waypoint_btn.grid_remove() this.waypoint_next_btn.grid_remove() @@ -108,8 +109,49 @@ def update_gui(): this.waypoint_next_btn.config(state=tk.NORMAL) this.clear_route_btn.grid() - +def update_gui(): + show_route_gui(True) + +def show_plot_gui(show=True): + if show: + this.waypoint_prev_btn.grid_remove() + this.waypoint_btn.grid_remove() + this.waypoint_next_btn.grid_remove() + this.jumpcounttxt_lbl.grid_remove() + this.clear_route_btn.grid_remove() + + this.plot_gui_btn.grid_remove() + this.csv_route_btn.grid_remove() + this.source_ac.grid() + this.dest_ac.grid() + this.range_entry.grid() + this.efficiency_slider.grid() + this.plot_route_btn.grid() + this.cancel_plot.grid() + + # Workaround because EDMC keeps switching the placeholder to bright white + if this.source_ac.get() == this.source_ac.placeholder: + this.source_ac.force_placeholder_color() + if this.dest_ac.get() == this.dest_ac.placeholder: + this.dest_ac.force_placeholder_color() + if this.range_entry.get() == this.range_entry.placeholder: + this.range_entry.force_placeholder_color() + show_route_gui(False) + + else: + this.source_ac.put_placeholder() + this.dest_ac.put_placeholder() + this.source_ac.grid_remove() + this.dest_ac.grid_remove() + this.range_entry.grid_remove() + this.efficiency_slider.grid_remove() + this.plot_gui_btn.grid_remove() + this.plot_route_btn.grid_remove() + this.cancel_plot.grid_remove() + this.plot_gui_btn.grid() + this.csv_route_btn.grid() + show_route_gui(True) def copy_waypoint(self=None): if sys.platform == "win32": @@ -128,7 +170,7 @@ def goto_prev_waypoint(self=None): if this.offset > 0: update_route(-1) -def new_route(self=None): +def plot_csv(self=None): filename = filedialog.askopenfilename(filetypes = (("csv files", "*.csv"),)) # show an "Open" dialog box and return the path to the selected file if filename.__len__() > 0: @@ -148,13 +190,94 @@ def new_route(self=None): copy_waypoint() update_gui() -def clear_route(self=None): - clear = confirmDialog.askyesno("SpanshRouter","Are you sure you want to clear the current route?") +def enable_plot_gui(enable): + if enable: + this.source_ac.config(state=tk.NORMAL) + this.source_ac.update_idletasks() + this.dest_ac.config(state=tk.NORMAL) + this.dest_ac.update_idletasks() + this.efficiency_slider.config(state=tk.NORMAL) + this.efficiency_slider.update_idletasks() + this.range_entry.config(state=tk.NORMAL) + this.range_entry.update_idletasks() + this.plot_route_btn.config(state=tk.NORMAL, text="Calculate") + this.plot_route_btn.update_idletasks() + this.cancel_plot.config(state=tk.NORMAL) + this.cancel_plot.update_idletasks() + else: + this.source_ac.config(state=tk.DISABLED) + this.source_ac.update_idletasks() + this.dest_ac.config(state=tk.DISABLED) + this.dest_ac.update_idletasks() + this.efficiency_slider.config(state=tk.DISABLED) + this.efficiency_slider.update_idletasks() + this.range_entry.config(state=tk.DISABLED) + this.range_entry.update_idletasks() + this.plot_route_btn.config(state=tk.DISABLED, text="Computing...") + this.plot_route_btn.update_idletasks() + this.cancel_plot.config(state=tk.DISABLED) + this.cancel_plot.update_idletasks() + +def plot_route(self=None): + try: + source = this.source_ac.get() + dest = this.dest_ac.get() + efficiency = this.efficiency_slider.get() + + if ( source and source != this.source_ac.placeholder and + dest and dest != this.dest_ac.placeholder ): + range_ly = float(this.range_entry.get()) + + job_url="https://spansh.co.uk/api/route?" + + results = requests.post(job_url, params={ + "efficiency": efficiency, + "range": range_ly, + "from": source, + "to": dest + }, headers={'User-Agent': "EDMC_SpanshRouter 1.0"}) + + if results.status_code == 202: + enable_plot_gui(False) + + while(True): + response = json.loads(results.content) + job = response["job"] + + results_url = "https://spansh.co.uk/api/results/" + job + route_response = requests.get(results_url) + if route_response.status_code != 202: + break + + if route_response.status_code == 200: + route = json.loads(route_response.content)["result"]["system_jumps"] + clear_route(show_dialog=False) + for waypoint in route: + this.route.append([waypoint["system"], str(waypoint["jumps"])]) + this.jumps_left += waypoint["jumps"] + enable_plot_gui(True) + show_plot_gui(False) + this.offset = 0 + this.next_stop = this.route[0][0] + copy_waypoint() + update_gui() + else: + sys.stderr.write("Failed to query plotted route from Spansh: code " + str(route_response.status_code) + route_response.text) + else: + sys.stderr.write("Failed to query route from Spansh: code " + str(results.status_code) + results.text) + + except: + pass + +def clear_route(self=None, show_dialog=True): + print("Show dialog =" + str(show_dialog)) + clear = confirmDialog.askyesno("SpanshRouter","Are you sure you want to clear the current route?") if show_dialog else True if clear: this.offset = 0 this.route = [] this.next_waypoint = "" + this.jumps_left = 0 try: os.remove(this.save_route_path) except: @@ -166,7 +289,6 @@ def clear_route(self=None): update_gui() - def update_route(direction=1): if direction > 0: this.jumps_left -= int(this.route[this.offset][1]) @@ -183,8 +305,11 @@ def update_route(direction=1): update_gui() copy_waypoint(this.parent) - def journal_entry(cmdr, is_beta, system, station, entry, state): + if entry["StarSystem"]: + this.source_ac.delete(0, tk.END) + this.source_ac.insert(0, entry["StarSystem"]) + this.source_ac["fg"] = this.source_ac.default_fg_color if (entry['event'] == 'FSDJump' or entry['event'] == 'Location') and entry["StarSystem"] == this.next_stop: update_route() elif entry['event'] in ['SupercruiseEntry', 'SupercruiseExit'] and entry['StarSystem'] == this.next_stop: @@ -192,30 +317,60 @@ def journal_entry(cmdr, is_beta, system, station, entry, state): elif entry['event'] == 'FSSDiscoveryScan' and entry['SystemName'] == this.next_stop: update_route() - def goto_update_page(self=None): webbrowser.open('https://github.com/CMDR-Kiel42/EDMC_SpanshRouter/releases') - def plugin_app(parent): this.parent = parent - this.frame = tk.Frame(parent) - + parentwidth = parent.winfo_width() + this.frame = tk.Frame(parent, borderwidth=2) + this.frame.grid(sticky=tk.NSEW) + + # Route info this.waypoint_prev_btn = tk.Button(this.frame, text="^", command=goto_prev_waypoint) this.waypoint_btn = tk.Button(this.frame, text=this.next_wp_label + this.next_stop, command=copy_waypoint) this.waypoint_next_btn = tk.Button(this.frame, text="v", command=goto_next_waypoint) + this.jumpcounttxt_lbl = tk.Label(this.frame, text=this.jumpcountlbl_txt + str(this.jumps_left)) - this.upload_route_btn = tk.Button(this.frame, text="Upload new route", command=new_route) + # Plotting GUI + this.source_ac = AutoCompleter(this.frame, "Source System", width=30) + this.dest_ac = AutoCompleter(this.frame, "Destination System", width=30) + this.range_entry = PlaceHolderEntry(this.frame, "Range (LY)", width=10) + this.efficiency_slider = tk.Scale(this.frame, from_=1, to=100, orient=tk.HORIZONTAL, label="Efficiency (%)") + this.efficiency_slider.set(60) + this.plot_gui_btn = tk.Button(this.frame, text="Plot route", command=show_plot_gui) + this.plot_route_btn = tk.Button(this.frame, text="Calculate", command=plot_route) + this.cancel_plot = tk.Button(this.frame, text="Cancel", command=lambda: show_plot_gui(False)) + + this.csv_route_btn = tk.Button(this.frame, text="Import CSV", command=plot_csv) this.clear_route_btn = tk.Button(this.frame, text="Clear route", command=clear_route) - this.waypoint_prev_btn.grid(row=0, columnspan=2) - this.waypoint_btn.grid(row=1, columnspan=2) - this.waypoint_next_btn.grid(row=2, columnspan=2) - this.upload_route_btn.grid(row=3, pady=10, padx=0) - this.clear_route_btn.grid(row=3,column=1) + row = 0 + this.waypoint_prev_btn.grid(row=row, columnspan=2) + row += 1 + this.waypoint_btn.grid(row=row, columnspan=2) + row += 1 + this.waypoint_next_btn.grid(row=row, columnspan=2) + row += 1 + this.source_ac.grid(row=row,columnspan=2, pady=(10,0)) # The AutoCompleter takes two rows to show the list when needed, so we skip one + row += 2 + this.dest_ac.grid(row=row,columnspan=2, pady=(10,0)) + row += 2 + this.range_entry.grid(row=row, pady=10, sticky=tk.W) + row += 1 + this.efficiency_slider.grid(row=row, pady=10, columnspan=2, sticky=tk.EW) + row += 1 + this.csv_route_btn.grid(row=row, pady=10, padx=0) + this.plot_route_btn.grid(row=row, pady=10, padx=0) + this.plot_gui_btn.grid(row=row, column=1, pady=10, padx=5, sticky=tk.W) + this.cancel_plot.grid(row=row, column=1, pady=10, padx=5, sticky=tk.E) + row += 1 + this.clear_route_btn.grid(row=row,column=1) + row += 1 + this.jumpcounttxt_lbl.grid(row=row, pady=5, sticky=tk.W) + row += 1 - this.jumpcounttxt_lbl = tk.Label(this.frame, text=this.jumpcountlbl_txt + str(this.jumps_left)) - this.jumpcounttxt_lbl.grid(row=4, pady=5, sticky=tk.W) + show_plot_gui(False) if not this.route.__len__() > 0: this.waypoint_prev_btn.grid_remove() @@ -226,7 +381,8 @@ def plugin_app(parent): if this.update_available: this.update_btn = tk.Button(this.frame, text="SpanshRouter update available for download!", command=goto_update_page) - this.update_btn.grid(row=5, pady=5, columnspan=2) + this.update_btn.grid(row=row, pady=5, columnspan=2) + row += 1 update_gui() diff --git a/version.json b/version.json index cb174d5..359a5b9 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -1.2.1 \ No newline at end of file +2.0.0 \ No newline at end of file