1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-06-09 11:52:27 +03:00

Add support for internal plugins

Settings tab shows current plugins.
This commit is contained in:
Jonathan Harris 2017-07-12 20:16:57 +01:00
parent 72efbadc59
commit 084128e797
6 changed files with 159 additions and 226 deletions

View File

@ -135,8 +135,8 @@ class AppWindow:
self.system.grid(row=3, column=1, sticky=tk.EW) self.system.grid(row=3, column=1, sticky=tk.EW)
self.station.grid(row=4, column=1, sticky=tk.EW) self.station.grid(row=4, column=1, sticky=tk.EW)
for plugname in plug.PLUGINS: for plugin in plug.PLUGINS:
appitem = plug.get_plugin_app(plugname, frame) appitem = plugin.get_app(frame)
if appitem: if appitem:
if isinstance(appitem, tuple) and len(appitem)==2: if isinstance(appitem, tuple) and len(appitem)==2:
row = frame.grid_size()[1] row = frame.grid_size()[1]
@ -489,7 +489,7 @@ class AppWindow:
self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy
# stuff we can do when not docked # stuff we can do when not docked
plug.notify_newdata(data) self.status['text'] = plug.notify_newdata(data) or ''
if config.getint('output') & config.OUT_SHIP: if config.getint('output') & config.OUT_SHIP:
loadout.export(data) loadout.export(data)
@ -733,7 +733,9 @@ class AppWindow:
return # Startup or in CQC return # Startup or in CQC
# Plugins # Plugins
plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry, monitor.state) err = plug.notify_journal_entry(monitor.cmdr, monitor.system, monitor.station, entry, monitor.state)
if err:
self.status['text'] = err
if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started: if entry['event'] in ['StartUp', 'LoadGame'] and monitor.started:
# Can start interaction monitoring # Can start interaction monitoring
@ -814,7 +816,9 @@ class AppWindow:
return return
# Currently we don't do anything with these events # Currently we don't do anything with these events
plug.notify_interaction(monitor.cmdr, entry) err = plug.notify_interaction(monitor.cmdr, entry)
if err:
self.status['text'] = err
def edsmpoll(self): def edsmpoll(self):
result = self.edsm.result result = self.edsm.result

View File

@ -123,6 +123,8 @@ class Config:
if not isdir(self.plugin_dir): if not isdir(self.plugin_dir):
mkdir(self.plugin_dir) mkdir(self.plugin_dir)
self.internal_plugin_dir = getattr(sys, 'frozen', False) and normpath(join(dirname(sys.executable), pardir, 'Resources')) or join(dirname(__file__), 'plugins')
self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous') self.default_journal_dir = join(NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0], 'Frontier Developments', 'Elite Dangerous')
self.default_interaction_dir = join(self.default_journal_dir, 'CommanderHistory') self.default_interaction_dir = join(self.default_journal_dir, 'CommanderHistory')
@ -183,6 +185,8 @@ class Config:
if not isdir(self.plugin_dir): if not isdir(self.plugin_dir):
mkdir(self.plugin_dir) mkdir(self.plugin_dir)
self.internal_plugin_dir = getattr(sys, 'frozen', False) and dirname(sys.executable) or join(dirname(__file__), 'plugins')
# expanduser in Python 2 on Windows doesn't handle non-ASCII - http://bugs.python.org/issue13207 # expanduser in Python 2 on Windows doesn't handle non-ASCII - http://bugs.python.org/issue13207
self.home = KnownFolderPath(FOLDERID_Profile) or u'\\' self.home = KnownFolderPath(FOLDERID_Profile) or u'\\'
@ -282,6 +286,8 @@ class Config:
if not isdir(self.plugin_dir): if not isdir(self.plugin_dir):
mkdir(self.plugin_dir) mkdir(self.plugin_dir)
self.internal_plugin_dir = join(dirname(__file__), 'plugins')
self.default_journal_dir = None self.default_journal_dir = None
self.default_interaction_dir = None self.default_interaction_dir = None

View File

@ -6,7 +6,7 @@
import codecs import codecs
from collections import OrderedDict from collections import OrderedDict
import os import os
from os.path import dirname, isfile, join, normpath from os.path import basename, dirname, isfile, join, normpath
import re import re
import sys import sys
from sys import platform from sys import platform
@ -166,14 +166,15 @@ if __name__ == "__main__":
import re import re
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
seen = {} seen = {}
for f in sorted([x for x in os.listdir('.') if x.endswith('.py')]): for f in sorted([x for x in os.listdir('.') if x.endswith('.py')] +
[join('plugins', x) for x in os.listdir('plugins') if x.endswith('.py')]):
with codecs.open(f, 'r', 'utf-8') as h: with codecs.open(f, 'r', 'utf-8') as h:
lineno = 0 lineno = 0
for line in h: for line in h:
lineno += 1 lineno += 1
match = regexp.search(line) match = regexp.search(line)
if match and not seen.get(match.group(2)): # only record first commented instance of a string if match and not seen.get(match.group(2)): # only record first commented instance of a string
seen[match.group(2)] = (match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + '[%s]' % f seen[match.group(2)] = (match.group(4) and (match.group(4)[1:].strip()) + '. ' or '') + '[%s]' % basename(f)
if seen: if seen:
template = codecs.open('L10n/en.template', 'w', 'utf-8') template = codecs.open('L10n/en.template', 'w', 'utf-8')
template.write('/* Language name */\n"%s" = "%s";\n\n' % (LANGUAGE_ID, 'English')) template.write('/* Language name */\n"%s" = "%s";\n\n' % (LANGUAGE_ID, 'English'))

252
plug.py
View File

@ -1,150 +1,151 @@
""" """
Plugin hooks for EDMC - Ian Norton Plugin hooks for EDMC - Ian Norton, Jonathan Harris
""" """
import os import os
import imp import imp
import sys import sys
import operator
import threading # We don't use it, but plugins might
from traceback import print_exc
if __debug__: from config import config, appname
from traceback import print_exc
from config import config # List of loaded Plugins
PLUGINS = []
"""
Dictionary of loaded plugin modules.
"""
PLUGINS = dict()
def find_plugins(): class Plugin(object):
"""
Look for plugin entry points. def __init__(self, name, loadfile):
:return: """
""" Load a single plugin
found = dict() :param name: module name
disabled = list() :param loadfile: the main .py file
plug_folders = os.listdir(config.plugin_dir) :raises Exception: Typically ImportError or OSError
for name in plug_folders: """
if name.endswith(".disabled"):
name, discard = name.rsplit(".", 1) self.name = name # Display name.
disabled.append(name) self.folder = name # basename of plugin folder. None for internal plugins.
continue self.module = None # None for disabled plugins.
loadfile = os.path.join(config.plugin_dir, name, "load.py")
if os.path.isfile(loadfile): if loadfile:
found[name] = loadfile sys.stdout.write('loading plugin %s\n' % name)
return found, disabled with open(loadfile, 'rb') as plugfile:
module = imp.load_module(name, plugfile, loadfile.encode(sys.getfilesystemencoding()),
('.py', 'r', imp.PY_SOURCE))
newname = module.plugin_start()
self.name = newname and unicode(newname) or name
self.module = module
else:
sys.stdout.write('plugin %s disabled\n' % name)
def _get_func(self, funcname):
"""
Get a function from a plugin, else return None if it isn't implemented.
:param funcname:
:return:
"""
return getattr(self.module, funcname, None)
def get_app(self, parent):
"""
If the plugin provides mainwindow content create and return it.
:param parent: the parent frame for this entry.
:return:
"""
try:
plugin_app = self._get_func('plugin_app')
return plugin_app and plugin_app(parent)
except:
print_exc()
return None
def get_prefs(self, parent):
"""
If the plugin provides a prefs frame, create and return it.
:param parent: the parent frame for this preference tab.
:return:
"""
try:
plugin_prefs = self._get_func('plugin_prefs')
return plugin_prefs and plugin_prefs(parent)
except:
print_exc()
return None
def load_plugins(): def load_plugins():
""" """
Load all found plugins Find and load all plugins
:return: :return:
""" """
found, disabled = find_plugins()
imp.acquire_lock() imp.acquire_lock()
for plugname in disabled:
sys.stdout.write("plugin {} disabled\n".format(plugname))
for plugname, loadfile in found.iteritems(): internal = []
try: for name in os.listdir(config.internal_plugin_dir):
sys.stdout.write("loading plugin {}\n".format(plugname)) if name.endswith('.py') and not name[0] in ['.', '_'] and not name.startswith(appname):
try:
# Add plugin's folder to Python's load path in case plugin has dependencies. plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir, name))
sys.path.append(os.path.dirname(loadfile)) plugin.folder = None # Suppress listing in Plugins prefs tab
internal.append(plugin)
with open(loadfile, "rb") as plugfile: except:
plugmod = imp.load_module(plugname, plugfile, loadfile.encode(sys.getfilesystemencoding()),
(".py", "r", imp.PY_SOURCE))
if hasattr(plugmod, "plugin_start"):
newname = plugmod.plugin_start()
PLUGINS[newname and unicode(newname) or plugname] = plugmod
except Exception as plugerr:
if __debug__:
print_exc() print_exc()
else: PLUGINS.extend(sorted(internal, key = lambda p: operator.attrgetter('name')(p).lower()))
sys.stderr.write('%s: %s\n' % (plugname, plugerr)) # appears in %TMP%/EDMarketConnector.log in packaged Windows app
found = []
for name in os.listdir(config.plugin_dir):
if name[0] == '.':
pass
elif name.endswith('.disabled'):
name, discard = name.rsplit('.', 1)
found.append(Plugin(name, None))
else:
try:
found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py')))
except:
print_exc()
PLUGINS.extend(sorted(found, key = lambda p: operator.attrgetter('name')(p).lower()))
imp.release_lock() imp.release_lock()
def _get_plugin_func(plugname, funcname):
"""
Get a function from a plugin, else return None if it isn't implemented.
:param plugname:
:param funcname:
:return:
"""
return getattr(PLUGINS[plugname], funcname, None)
def get_plugin_app(plugname, parent):
"""
If the plugin provides mainwindow content create and return it.
:param plugname: name of the plugin
:param parent: the parent frame for this entry.
:return:
"""
plugin_app = _get_plugin_func(plugname, "plugin_app")
if plugin_app:
return plugin_app(parent)
return None
def get_plugin_prefs(plugname, parent):
"""
If the plugin provides a prefs frame, create and return it.
:param plugname: name of the plugin
:param parent: the parent frame for this preference tab.
:return:
"""
plugin_prefs = _get_plugin_func(plugname, "plugin_prefs")
if plugin_prefs:
return plugin_prefs(parent)
return None
def notify_prefs_changed(): def notify_prefs_changed():
""" """
Notify each plugin that the settings dialog has been closed. Notify each plugin that the settings dialog has been closed.
:return: :return:
""" """
for plugname in PLUGINS: for plugin in PLUGINS:
prefs_changed = _get_plugin_func(plugname, "prefs_changed") prefs_changed = plugin._get_func('prefs_changed')
if prefs_changed: if prefs_changed:
try: try:
prefs_changed() prefs_changed()
except Exception as plugerr: except:
if __debug__: print_exc()
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
def notify_journal_entry(cmdr, system, station, entry, state): def notify_journal_entry(cmdr, system, station, entry, cmdr_state):
""" """
Send a journal entry to each plugin. Send a journal entry to each plugin.
:param cmdr: The Cmdr name, or None if not yet known :param cmdr: The Cmdr name, or None if not yet known
:param system: The current system, or None if not yet known :param system: The current system, or None if not yet known
:param station: The current station, or None if not docked or not yet known :param station: The current station, or None if not docked or not yet known
:param entry: The journal entry as a dictionary :param entry: The journal entry as a dictionary
:param state: A dictionary containing info about the Cmdr, current ship and cargo :param cmdr_state: A dictionary containing info about the Cmdr, current ship and cargo
:return: :return: Error message from the first plugin that returns one (if any)
""" """
for plugname in PLUGINS: error = None
journal_entry = _get_plugin_func(plugname, "journal_entry") for plugin in PLUGINS:
journal_entry = plugin._get_func('journal_entry')
if journal_entry: if journal_entry:
try: try:
# Pass a copy of the journal entry in case the callee modifies it # Pass a copy of the journal entry in case the callee modifies it
if journal_entry.func_code.co_argcount == 4: if journal_entry.func_code.co_argcount == 4:
journal_entry(cmdr, system, station, dict(entry)) error = error or journal_entry(cmdr, system, station, dict(entry))
else: else:
journal_entry(cmdr, system, station, dict(entry), dict(state)) error = error or journal_entry(cmdr, system, station, dict(entry), dict(cmdr_state))
except Exception as plugerr: except:
if __debug__: print_exc()
print_exc() return error
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
def notify_interaction(cmdr, entry): def notify_interaction(cmdr, entry):
@ -152,19 +153,18 @@ def notify_interaction(cmdr, entry):
Send an interaction entry to each plugin. Send an interaction entry to each plugin.
:param cmdr: The piloting Cmdr name :param cmdr: The piloting Cmdr name
:param entry: The interaction entry as a dictionary :param entry: The interaction entry as a dictionary
:return: :return: Error message from the first plugin that returns one (if any)
""" """
for plugname in PLUGINS: error = None
interaction = _get_plugin_func(plugname, "interaction") for plugin in PLUGINS:
interaction = plugin._get_func('interaction')
if interaction: if interaction:
try: try:
# Pass a copy of the interaction entry in case the callee modifies it # Pass a copy of the interaction entry in case the callee modifies it
interaction(cmdr, dict(entry)) error = error or interaction(cmdr, dict(entry))
except Exception as plugerr: except:
if __debug__: print_exc()
print_exc() return error
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
def notify_system_changed(timestamp, system, coordinates): def notify_system_changed(timestamp, system, coordinates):
@ -176,34 +176,30 @@ def notify_system_changed(timestamp, system, coordinates):
deprecated:: 2.2 deprecated:: 2.2
Use :func:`journal_entry` with the 'FSDJump' event. Use :func:`journal_entry` with the 'FSDJump' event.
""" """
for plugname in PLUGINS: for plugin in PLUGINS:
system_changed = _get_plugin_func(plugname, "system_changed") system_changed = plugin._get_func('system_changed')
if system_changed: if system_changed:
try: try:
if system_changed.func_code.co_argcount == 2: if system_changed.func_code.co_argcount == 2:
system_changed(timestamp, system) system_changed(timestamp, system)
else: else:
system_changed(timestamp, system, coordinates) system_changed(timestamp, system, coordinates)
except Exception as plugerr: except:
if __debug__: print_exc()
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
def notify_newdata(data): def notify_newdata(data):
""" """
Send the latest EDMC data from the FD servers to each plugin Send the latest EDMC data from the FD servers to each plugin
:param data: :param data:
:return: :return: Error message from the first plugin that returns one (if any)
""" """
for plugname in PLUGINS: error = None
cmdr_data = _get_plugin_func(plugname, "cmdr_data") for plugin in PLUGINS:
cmdr_data = plugin._get_func('cmdr_data')
if cmdr_data: if cmdr_data:
try: try:
cmdr_data(data) error = error or cmdr_data(data)
except Exception as plugerr: except:
if __debug__: print_exc()
print_exc() return error
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))

View File

@ -1,78 +0,0 @@
"""
A Skeleton EDMC Plugin
"""
import sys
import ttk
import Tkinter as tk
from config import applongname, appversion
import myNotebook as nb
def plugin_start():
"""
Start this plugin
:return: Plugin name
"""
sys.stderr.write("example plugin started\n") # appears in %TMP%/EDMarketConnector.log in packaged Windows app
return 'About'
def plugin_prefs(parent):
"""
Return a TK Frame for adding to the EDMC settings dialog.
"""
frame = nb.Frame(parent)
nb.Label(frame, text="{NAME} {VER}".format(NAME=applongname, VER=appversion)).grid(sticky=tk.W)
nb.Label(frame).grid() # spacer
nb.Label(frame, text="Fly Safe!").grid(sticky=tk.W)
nb.Label(frame).grid() # spacer
if cmdr_data.last is not None:
datalen = len(str(cmdr_data.last))
nb.Label(frame, text="FD sent {} chars".format(datalen)).grid(sticky=tk.W)
return frame
def plugin_app(parent):
"""
Return a TK Widget for the EDMC main window.
:param parent:
:return:
"""
plugin_app.status = tk.Label(parent, text="---")
return plugin_app.status
def journal_entry(cmdr, system, station, entry, state):
"""
E:D client made a journal entry
:param cmdr: The Cmdr name, or None if not yet known
:param system: The current system, or None if not yet known
:param station: The current station, or None if not docked or not yet known
:param entry: The journal entry as a dictionary
:param state: A dictionary containing info about the Cmdr, current ship and cargo
:return:
"""
if entry['event'] == 'FSDJump':
# We arrived at a new system!
if 'StarPos' in entry:
sys.stderr.write("Arrived at {} ({},{},{})\n".format(entry['StarSystem'], *tuple(entry['StarPos'])))
else:
sys.stderr.write("Arrived at {}\n".format(entry['StarSystem']))
def cmdr_data(data):
"""
Obtained new data from Frontier about our commander, location and ships
:param data:
:return:
"""
cmdr_data.last = data
plugin_app.status['text'] = "Got new data ({} chars)".format(len(str(data)))
sys.stderr.write("Got new data ({} chars)\n".format(len(str(data))))
cmdr_data.last = None

View File

@ -211,10 +211,10 @@ class PreferencesDialog(tk.Toplevel):
notebook.add(edsmframe, text='EDSM') # Not translated notebook.add(edsmframe, text='EDSM') # Not translated
# build plugin prefs tabs # build plugin prefs tabs
for plugname in plug.PLUGINS: for plugin in plug.PLUGINS:
plugframe = plug.get_plugin_prefs(plugname, notebook) plugframe = plugin.get_prefs(notebook)
if plugframe: if plugframe:
notebook.add(plugframe, text=plugname) notebook.add(plugframe, text=plugin.name)
configframe = nb.Frame(notebook) configframe = nb.Frame(notebook)
configframe.columnconfigure(1, weight=1) configframe.columnconfigure(1, weight=1)
@ -326,11 +326,8 @@ class PreferencesDialog(tk.Toplevel):
plugdir = tk.StringVar() plugdir = tk.StringVar()
plugdir.set(config.plugin_dir) plugdir.set(config.plugin_dir)
plugnames, disabled_plugins = plug.find_plugins()
nb.Label(plugsframe, text=_('Plugins folder')+':').grid(padx=PADX, sticky=tk.W) # Section heading in settings nb.Label(plugsframe, text=_('Plugins folder')+':').grid(padx=PADX, sticky=tk.W) # Section heading in settings
plugdirentry = nb.Entry(plugsframe, plugdirentry = nb.Entry(plugsframe, justify=tk.LEFT)
justify=tk.LEFT)
self.displaypath(plugdir, plugdirentry) self.displaypath(plugdir, plugdirentry)
plugdirentry.grid(row=10, padx=PADX, sticky=tk.EW) plugdirentry.grid(row=10, padx=PADX, sticky=tk.EW)
@ -340,16 +337,23 @@ class PreferencesDialog(tk.Toplevel):
nb.Label(plugsframe, text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to it's folder name").format(EXT='.disabled')).grid( # Help text in settings nb.Label(plugsframe, text=_("Tip: You can disable a plugin by{CR}adding '{EXT}' to it's folder name").format(EXT='.disabled')).grid( # Help text in settings
columnspan=2, padx=PADX, pady=10, sticky=tk.NSEW) columnspan=2, padx=PADX, pady=10, sticky=tk.NSEW)
if len(plugnames): enabled_plugins = [x for x in plug.PLUGINS if x.folder and x.module]
if len(enabled_plugins):
ttk.Separator(plugsframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW) ttk.Separator(plugsframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW)
nb.Label(plugsframe, text=_('Enabled Plugins')+':').grid(padx=PADX, sticky=tk.W) # List of plugins in settings nb.Label(plugsframe, text=_('Enabled Plugins')+':').grid(padx=PADX, sticky=tk.W) # List of plugins in settings
for plugname in plugnames: for plugin in enabled_plugins:
nb.Label(plugsframe, text=plugname).grid(columnspan=2, padx=PADX*2, sticky=tk.W) if plugin.name == plugin.folder:
label = nb.Label(plugsframe, text=plugin.name)
else:
label = nb.Label(plugsframe, text='%s (%s)' % (plugin.folder, plugin.name))
label.grid(columnspan=2, padx=PADX*2, sticky=tk.W)
disabled_plugins = [x for x in plug.PLUGINS if x.folder and not x.module]
if len(disabled_plugins): if len(disabled_plugins):
ttk.Separator(plugsframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW) ttk.Separator(plugsframe, orient=tk.HORIZONTAL).grid(columnspan=3, padx=PADX, pady=PADY * 8, sticky=tk.EW)
nb.Label(plugsframe, text=_('Disabled Plugins')+':').grid(padx=PADX, sticky=tk.W) # List of plugins in settings nb.Label(plugsframe, text=_('Disabled Plugins')+':').grid(padx=PADX, sticky=tk.W) # List of plugins in settings
for plugname in disabled_plugins: for plugin in disabled_plugins:
nb.Label(plugsframe, text=plugname).grid(columnspan=2, padx=PADX*2, sticky=tk.W) nb.Label(plugsframe, text=plugin.name).grid(columnspan=2, padx=PADX*2, sticky=tk.W)
notebook.add(plugsframe, text=_('Plugins')) # Tab heading in settings notebook.add(plugsframe, text=_('Plugins')) # Tab heading in settings