1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-14 00:07:14 +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.station.grid(row=4, column=1, sticky=tk.EW)
for plugname in plug.PLUGINS:
appitem = plug.get_plugin_app(plugname, frame)
for plugin in plug.PLUGINS:
appitem = plugin.get_app(frame)
if appitem:
if isinstance(appitem, tuple) and len(appitem)==2:
row = frame.grid_size()[1]
@ -489,7 +489,7 @@ class AppWindow:
self.edit_menu.entryconfigure(0, state=tk.NORMAL) # Copy
# 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:
loadout.export(data)
@ -733,7 +733,9 @@ class AppWindow:
return # Startup or in CQC
# 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:
# Can start interaction monitoring
@ -814,7 +816,9 @@ class AppWindow:
return
# 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):
result = self.edsm.result

View File

@ -123,6 +123,8 @@ class Config:
if not isdir(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_interaction_dir = join(self.default_journal_dir, 'CommanderHistory')
@ -183,6 +185,8 @@ class Config:
if not isdir(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
self.home = KnownFolderPath(FOLDERID_Profile) or u'\\'
@ -282,6 +286,8 @@ class Config:
if not isdir(self.plugin_dir):
mkdir(self.plugin_dir)
self.internal_plugin_dir = join(dirname(__file__), 'plugins')
self.default_journal_dir = None
self.default_interaction_dir = None

View File

@ -6,7 +6,7 @@
import codecs
from collections import OrderedDict
import os
from os.path import dirname, isfile, join, normpath
from os.path import basename, dirname, isfile, join, normpath
import re
import sys
from sys import platform
@ -166,14 +166,15 @@ if __name__ == "__main__":
import re
regexp = re.compile(r'''_\([ur]?(['"])(((?<!\\)\\\1|.)+?)\1\)[^#]*(#.+)?''') # match a single line python literal
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:
lineno = 0
for line in h:
lineno += 1
match = regexp.search(line)
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:
template = codecs.open('L10n/en.template', 'w', 'utf-8')
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 imp
import sys
import operator
import threading # We don't use it, but plugins might
from traceback import print_exc
if __debug__:
from traceback import print_exc
from config import config, appname
from config import config
"""
Dictionary of loaded plugin modules.
"""
PLUGINS = dict()
# List of loaded Plugins
PLUGINS = []
def find_plugins():
"""
Look for plugin entry points.
:return:
"""
found = dict()
disabled = list()
plug_folders = os.listdir(config.plugin_dir)
for name in plug_folders:
if name.endswith(".disabled"):
name, discard = name.rsplit(".", 1)
disabled.append(name)
continue
loadfile = os.path.join(config.plugin_dir, name, "load.py")
if os.path.isfile(loadfile):
found[name] = loadfile
return found, disabled
class Plugin(object):
def __init__(self, name, loadfile):
"""
Load a single plugin
:param name: module name
:param loadfile: the main .py file
:raises Exception: Typically ImportError or OSError
"""
self.name = name # Display name.
self.folder = name # basename of plugin folder. None for internal plugins.
self.module = None # None for disabled plugins.
if loadfile:
sys.stdout.write('loading plugin %s\n' % name)
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():
"""
Load all found plugins
Find and load all plugins
:return:
"""
found, disabled = find_plugins()
imp.acquire_lock()
for plugname in disabled:
sys.stdout.write("plugin {} disabled\n".format(plugname))
for plugname, loadfile in found.iteritems():
try:
sys.stdout.write("loading plugin {}\n".format(plugname))
# Add plugin's folder to Python's load path in case plugin has dependencies.
sys.path.append(os.path.dirname(loadfile))
with open(loadfile, "rb") as plugfile:
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__:
internal = []
for name in os.listdir(config.internal_plugin_dir):
if name.endswith('.py') and not name[0] in ['.', '_'] and not name.startswith(appname):
try:
plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir, name))
plugin.folder = None # Suppress listing in Plugins prefs tab
internal.append(plugin)
except:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr)) # appears in %TMP%/EDMarketConnector.log in packaged Windows app
PLUGINS.extend(sorted(internal, key = lambda p: operator.attrgetter('name')(p).lower()))
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()
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():
"""
Notify each plugin that the settings dialog has been closed.
:return:
"""
for plugname in PLUGINS:
prefs_changed = _get_plugin_func(plugname, "prefs_changed")
for plugin in PLUGINS:
prefs_changed = plugin._get_func('prefs_changed')
if prefs_changed:
try:
prefs_changed()
except Exception as plugerr:
if __debug__:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
except:
print_exc()
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.
: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:
:param cmdr_state: A dictionary containing info about the Cmdr, current ship and cargo
:return: Error message from the first plugin that returns one (if any)
"""
for plugname in PLUGINS:
journal_entry = _get_plugin_func(plugname, "journal_entry")
error = None
for plugin in PLUGINS:
journal_entry = plugin._get_func('journal_entry')
if journal_entry:
try:
# Pass a copy of the journal entry in case the callee modifies it
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:
journal_entry(cmdr, system, station, dict(entry), dict(state))
except Exception as plugerr:
if __debug__:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
error = error or journal_entry(cmdr, system, station, dict(entry), dict(cmdr_state))
except:
print_exc()
return error
def notify_interaction(cmdr, entry):
@ -152,19 +153,18 @@ def notify_interaction(cmdr, entry):
Send an interaction entry to each plugin.
:param cmdr: The piloting Cmdr name
: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:
interaction = _get_plugin_func(plugname, "interaction")
error = None
for plugin in PLUGINS:
interaction = plugin._get_func('interaction')
if interaction:
try:
# Pass a copy of the interaction entry in case the callee modifies it
interaction(cmdr, dict(entry))
except Exception as plugerr:
if __debug__:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
error = error or interaction(cmdr, dict(entry))
except:
print_exc()
return error
def notify_system_changed(timestamp, system, coordinates):
@ -176,34 +176,30 @@ def notify_system_changed(timestamp, system, coordinates):
deprecated:: 2.2
Use :func:`journal_entry` with the 'FSDJump' event.
"""
for plugname in PLUGINS:
system_changed = _get_plugin_func(plugname, "system_changed")
for plugin in PLUGINS:
system_changed = plugin._get_func('system_changed')
if system_changed:
try:
if system_changed.func_code.co_argcount == 2:
system_changed(timestamp, system)
else:
system_changed(timestamp, system, coordinates)
except Exception as plugerr:
if __debug__:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
except:
print_exc()
def notify_newdata(data):
"""
Send the latest EDMC data from the FD servers to each plugin
:param data:
:return:
:return: Error message from the first plugin that returns one (if any)
"""
for plugname in PLUGINS:
cmdr_data = _get_plugin_func(plugname, "cmdr_data")
error = None
for plugin in PLUGINS:
cmdr_data = plugin._get_func('cmdr_data')
if cmdr_data:
try:
cmdr_data(data)
except Exception as plugerr:
if __debug__:
print_exc()
else:
sys.stderr.write('%s: %s\n' % (plugname, plugerr))
error = error or cmdr_data(data)
except:
print_exc()
return error

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
# build plugin prefs tabs
for plugname in plug.PLUGINS:
plugframe = plug.get_plugin_prefs(plugname, notebook)
for plugin in plug.PLUGINS:
plugframe = plugin.get_prefs(notebook)
if plugframe:
notebook.add(plugframe, text=plugname)
notebook.add(plugframe, text=plugin.name)
configframe = nb.Frame(notebook)
configframe.columnconfigure(1, weight=1)
@ -326,11 +326,8 @@ class PreferencesDialog(tk.Toplevel):
plugdir = tk.StringVar()
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
plugdirentry = nb.Entry(plugsframe,
justify=tk.LEFT)
plugdirentry = nb.Entry(plugsframe, justify=tk.LEFT)
self.displaypath(plugdir, plugdirentry)
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
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)
nb.Label(plugsframe, text=_('Enabled Plugins')+':').grid(padx=PADX, sticky=tk.W) # List of plugins in settings
for plugname in plugnames:
nb.Label(plugsframe, text=plugname).grid(columnspan=2, padx=PADX*2, sticky=tk.W)
for plugin in enabled_plugins:
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):
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
for plugname in disabled_plugins:
nb.Label(plugsframe, text=plugname).grid(columnspan=2, padx=PADX*2, sticky=tk.W)
for plugin in disabled_plugins:
nb.Label(plugsframe, text=plugin.name).grid(columnspan=2, padx=PADX*2, sticky=tk.W)
notebook.add(plugsframe, text=_('Plugins')) # Tab heading in settings