1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-14 16:27:13 +03:00

Merge branch 'multi_account'

This commit is contained in:
Jonathan Harris 2017-02-03 14:00:39 +00:00
commit 0022f78e62
11 changed files with 297 additions and 116 deletions

View File

@ -82,7 +82,11 @@ try:
config.set('querytime', getmtime(args.j))
else:
session = companion.Session()
session.login(config.get('username'), config.get('password'))
if config.get('cmdrs'):
username = config.get('fdev_usernames')[0]
session.login(username, config.get_password(username))
else: # <= 2.25 not yet migrated
session.login(config.get('username'), config.get('password'))
querytime = int(time())
data = session.query()
config.set('querytime', querytime)

View File

@ -6,6 +6,7 @@ from sys import platform
from collections import OrderedDict
from functools import partial
import json
import keyring
from os import chdir, mkdir
from os.path import dirname, expanduser, isdir, join
import re
@ -28,8 +29,9 @@ if __debug__:
signal.signal(signal.SIGTERM, lambda sig, frame: pdb.Pdb().set_trace(frame))
from config import appname, applongname, config
if platform == 'win32' and getattr(sys, 'frozen', False):
chdir(dirname(sys.path[0]))
if getattr(sys, 'frozen', False):
if platform == 'win32':
chdir(dirname(sys.path[0]))
# By default py2exe tries to write log to dirname(sys.executable) which fails when installed
import tempfile
sys.stdout = sys.stderr = open(join(tempfile.gettempdir(), '%s.log' % appname), 'wt', 0) # unbuffered
@ -239,8 +241,6 @@ class AppWindow:
tk.Label(self.blank_menubar).grid()
theme.register_alternate((self.menubar, self.theme_menubar, self.blank_menubar), {'row':0, 'columnspan':2, 'sticky':tk.NSEW})
self.set_labels()
# update geometry
if config.get('geometry'):
match = re.match('\+([\-\d]+)\+([\-\d]+)', config.get('geometry'))
@ -281,17 +281,42 @@ class AppWindow:
# Load updater after UI creation (for WinSparkle)
import update
self.updater = update.Updater(self.w)
if not getattr(sys, 'frozen', False):
self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps
# First run
if not config.get('username') or not config.get('password'):
prefs.PreferencesDialog(self.w, self.postprefs)
else:
self.login()
try:
config.get_password('') # Prod SecureStorage on Linux to initialise
except RuntimeError:
pass
# Migration from <= 2.25
if not config.get('cmdrs') and config.get('username') and config.get('password'):
try:
self.session.login(config.get('username'), config.get('password'))
data = self.session.query()
prefs.migrate(data['commander']['name'])
except:
if __debug__: print_exc()
self.postprefs() # Companion login happens in callback from monitor
if keyring.get_keyring().priority < 1:
self.status['text'] = 'Warning: Storing passwords as text' # Shouldn't happen unless no secure storage on Linux
# Try to obtain exclusive lock on journal cache, even if we don't need it yet
if not eddn.load():
self.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing
# callback after the Preferences dialog is applied
def postprefs(self):
self.set_labels() # in case language has changed
self.login() # in case credentials gave changed
# (Re-)install hotkey monitoring
hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods'))
# (Re-)install log monitoring
if not monitor.start(self.w):
self.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2
# set main window labels, e.g. after language change
def set_labels(self):
@ -328,11 +353,16 @@ class AppWindow:
self.edit_menu.entryconfigure(0, label=_('Copy')) # As in Copy and Paste
def login(self):
self.status['text'] = _('Logging in...')
if not self.status['text']:
self.status['text'] = _('Logging in...')
self.button['state'] = self.theme_button['state'] = tk.DISABLED
self.w.update_idletasks()
try:
self.session.login(config.get('username'), config.get('password'))
if not monitor.cmdr or not config.get('cmdrs') or monitor.cmdr not in config.get('cmdrs'):
raise companion.CredentialsError()
idx = config.get('cmdrs').index(monitor.cmdr)
username = config.get('fdev_usernames')[idx]
self.session.login(username, config.get_password(username))
self.status['text'] = ''
except companion.VerificationRequired:
return prefs.AuthenticationDialog(self.w, partial(self.verify, self.login))
@ -341,21 +371,6 @@ class AppWindow:
except Exception as e:
if __debug__: print_exc()
self.status['text'] = unicode(e)
if not getattr(sys, 'frozen', False):
self.updater.checkForUpdates() # Sparkle / WinSparkle does this automatically for packaged apps
# Try to obtain exclusive lock on journal cache, even if we don't need it yet
if not eddn.load():
self.status['text'] = 'Error: Is another copy of this app already running?' # Shouldn't happen - don't bother localizing
# (Re-)install hotkey monitoring
hotkeymgr.register(self.w, config.getint('hotkey_code'), config.getint('hotkey_mods'))
# (Re-)install log monitoring
if not monitor.start(self.w):
self.status['text'] = 'Error: Check %s' % _('E:D journal file location') # Location of the new Journal file in E:D 2.2
self.cooldown()
# callback after verification code
@ -375,7 +390,7 @@ class AppWindow:
auto_update = not event
play_sound = (auto_update or int(event.type) == self.EVENT_VIRTUAL) and not config.getint('hotkey_mute')
if (monitor.cmdr and not monitor.mode) or monitor.is_beta:
if not monitor.cmdr or not monitor.mode or monitor.is_beta:
return # In CQC or Beta - do nothing
if not retrying:
@ -485,7 +500,7 @@ class AppWindow:
self.status['text'] = ''
# Update credits and ship info and send to EDSM
if config.getint('output') & config.OUT_SYS_EDSM and not monitor.is_beta:
if config.getint('output') & config.OUT_SYS_EDSM:
try:
if data['commander'].get('credits') is not None:
monitor.credits = (data['commander']['credits'], data['commander'].get('debt', 0))
@ -576,12 +591,12 @@ class AppWindow:
self.w.update_idletasks()
# Send interesting events to EDSM
if config.getint('output') & config.OUT_SYS_EDSM and not monitor.is_beta:
if config.getint('output') & config.OUT_SYS_EDSM and not monitor.is_beta and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs') and config.get('edsm_usernames')[config.get('cmdrs').index(monitor.cmdr)]:
self.status['text'] = _('Sending data to EDSM...')
self.w.update_idletasks()
try:
# Update system status on startup
if monitor.mode and not entry['event']:
if monitor.mode and monitor.system and not entry['event']:
self.edsm.lookup(monitor.system)
# Send credits to EDSM on new game (but not on startup - data might be old)
@ -616,6 +631,18 @@ class AppWindow:
self.status['text'] = ''
self.edsmpoll()
# Companion login - do this after EDSM so any EDSM errors don't mask login errors
if entry['event'] in [None, 'NewCommander', 'LoadGame'] and monitor.cmdr and not monitor.is_beta:
if config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'):
prefs.make_current(monitor.cmdr)
self.login()
elif config.get('cmdrs') and entry['event'] == 'NewCommander':
cmdrs = config.get('cmdrs')
cmdrs[0] = monitor.cmdr # New Cmdr uses same credentials as old
config.set('cmdrs', cmdrs)
else:
prefs.PreferencesDialog(self.w, self.postprefs) # First run or failed migration
if not entry['event'] or not monitor.mode:
return # Startup or in CQC
@ -695,7 +722,7 @@ class AppWindow:
def shipyard_url(self, shipname=None):
if (monitor.cmdr and not monitor.mode) or monitor.is_beta:
if not monitor.cmdr or not monitor.mode or monitor.is_beta:
return False # In CQC or Beta - do nothing
self.status['text'] = _('Fetching data...')
@ -839,14 +866,40 @@ class AppWindow:
# Run the app
if __name__ == "__main__":
# Ensure only one copy of the app is running. OSX does this automatically. Linux TODO.
# Ensure only one copy of the app is running under this user account. OSX does this automatically. Linux TODO.
if platform == 'win32':
import ctypes
h = ctypes.windll.user32.FindWindowW(u'TkTopLevel', unicode(applongname))
if h:
ctypes.windll.user32.ShowWindow(h, 9) # SW_RESTORE
ctypes.windll.user32.SetForegroundWindow(h) # Probably not necessary
sys.exit(0)
from ctypes.wintypes import *
EnumWindows = ctypes.windll.user32.EnumWindows
EnumWindowsProc = ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
GetClassName = ctypes.windll.user32.GetClassNameW
GetClassName.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowText = ctypes.windll.user32.GetWindowTextW
GetWindowText.argtypes = [HWND, LPWSTR, ctypes.c_int]
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
GetProcessHandleFromHwnd = ctypes.windll.oleacc.GetProcessHandleFromHwnd
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow
ShowWindow = ctypes.windll.user32.ShowWindow
def WindowTitle(h):
if h:
l = GetWindowTextLength(h) + 1
buf = ctypes.create_unicode_buffer(l)
if GetWindowText(h, buf, l):
return buf.value
return None
def enumwindowsproc(hWnd, lParam):
# class name limited to 256 - https://msdn.microsoft.com/en-us/library/windows/desktop/ms633576
cls = ctypes.create_unicode_buffer(257)
if GetClassName(hWnd, cls, 257) and cls.value == 'TkTopLevel' and WindowTitle(hWnd) == applongname and GetProcessHandleFromHwnd(hWnd):
# If GetProcessHandleFromHwnd succeeds then the app is already running as this user
ShowWindow(hWnd, 9) # SW_RESTORE
SetForegroundWindow(hWnd)
sys.exit(0)
return True
EnumWindows(EnumWindowsProc(enumwindowsproc), 0)
root = tk.Tk()
app = AppWindow(root)

View File

@ -145,6 +145,14 @@ If 2 this problem may or may not resolve itself in time.
This problem is tracked as [Issue #165](https://github.com/Marginal/EDMarketConnector/issues/165).
### Credentials settings are greyed out
You can't edit your Username/Password or EDSM Commander Name/API Key while Elite: Dangerous is at the Main Menu or in Beta. You will be able to edit these values once you've entered the (non-Beta) game.
### I run two instances of E:D simultaneously, but I can't run two instances of EDMC
EDMC supports this scenario if you run the second instance of E:D in a *different* user account - e.g. using `runas` on Windows. Run the second instance of EDMC in the same user account as the second instance of E:D.
EDMC doesn't support running two instances of E:D in the *same* user account. EDMC will only respond to the instance of E:D that you ran last.
### Error: Can't connect to EDDN
EDMC needs to talk to eddn-gateway.elite-markets.net on port 8080. If you consistently receive this error check that your router or VPN configuration allows port 8080 / tcp outbound.
@ -158,17 +166,17 @@ Download and extract the source code of the [latest release](https://github.com/
Mac:
* Requires the Python “requests” and “watchdog” modules, plus an up-to-date “py2app” module if you also want to package the app - install these with `easy_install -U requests watchdog py2app` .
* Requires the Python “keyring”, “requests” and “watchdog” modules, plus an up-to-date “py2app” module if you also want to package the app - install these with `easy_install -U keyring requests watchdog py2app` .
* Run with `./EDMarketConnector.py` .
Windows:
* Requires Python2.7 and the Python “requests” and “watchdog” modules.
* Requires Python2.7 and the Python “keyring”, “requests” and “watchdog” modules, plus “py2exe” 0.6 if you also want to package the app.
* Run with `EDMarketConnector.py` .
Linux:
* Requires the Python “imaging-tk”, “iniparse” and “requests” modules. On Debian-based systems install these with `sudo apt-get install python-imaging-tk python-iniparse python-requests` .
* Requires the Python “imaging-tk”, “iniparse”, “keyring” and “requests” modules. On Debian-based systems install these with `sudo apt-get install python-imaging-tk python-iniparse python-keyring python-requests` .
* Run with `./EDMarketConnector.py` .
Command-line
@ -243,6 +251,7 @@ Acknowledgements
* Thanks to Taras Velychko for the Ukranian translation.
* Thanks to [James Muscat](https://github.com/jamesremuscat) for [EDDN](https://github.com/jamesremuscat/EDDN/wiki) and to [Cmdr Anthor](https://github.com/AnthorNet) for the [stats](http://eddn-gateway.elite-markets.net/).
* Thanks to [Andargor](https://github.com/Andargor) for the idea of using the “Companion” interface in [edce-client](https://github.com/Andargor/edce-client).
* Uses [Python Keyring Lib](https://github.com/jaraco/keyring) by Jason R. Coombs, Kang Zhang, et al.
* Uses [Sparkle](https://github.com/sparkle-project/Sparkle) by [Andy Matuschak](http://andymatuschak.org/) and the [Sparkle Project](https://github.com/sparkle-project).
* Uses [WinSparkle](https://github.com/vslavik/winsparkle/wiki) by [Václav Slavík](https://github.com/vslavik).
* Uses [OneSky](http://www.oneskyapp.com/) for [translation management](https://marginal.oneskyapp.com/collaboration/project?id=52710).

View File

@ -1,9 +1,10 @@
import requests
from collections import defaultdict
from cookielib import LWPCookieJar
import hashlib
import numbers
import os
from os.path import dirname, join
from os.path import dirname, isfile, join
import sys
from sys import platform
import time
@ -178,6 +179,7 @@ class Session:
def __init__(self):
self.state = Session.STATE_INIT
self.credentials = None
self.session = None
# yuck suppress InsecurePlatformWarning under Python < 2.7.9 which lacks SNI support
try:
@ -189,14 +191,6 @@ class Session:
if platform=='win32' and getattr(sys, 'frozen', False):
os.environ['REQUESTS_CA_BUNDLE'] = join(dirname(sys.executable), 'cacert.pem')
self.session = requests.Session()
self.session.headers['User-Agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257'
self.session.cookies = LWPCookieJar(join(config.app_dir, 'cookies.txt'))
try:
self.session.cookies.load()
except IOError:
pass
def login(self, username=None, password=None):
if (not username or not password):
if not self.credentials:
@ -208,8 +202,20 @@ class Session:
if self.credentials == credentials and self.state == Session.STATE_OK:
return # already logged in
if self.credentials and self.credentials['email'] != credentials['email']: # changed account
self.session.cookies.clear()
if not self.credentials or self.credentials['email'] != credentials['email']: # changed account
self.close()
self.session = requests.Session()
self.session.headers['User-Agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257'
cookiefile = join(config.app_dir, 'cookies-%s.txt' % hashlib.md5(credentials['email']).hexdigest())
if not isfile(cookiefile) and isfile(join(config.app_dir, 'cookies.txt')):
os.rename(join(config.app_dir, 'cookies.txt'), cookiefile) # migration from <= 2.25
self.session.cookies = LWPCookieJar(cookiefile)
try:
self.session.cookies.load()
except IOError:
pass
self.credentials = credentials
self.state = Session.STATE_INIT
try:
@ -237,7 +243,7 @@ class Session:
r = self.session.post(URL_CONFIRM, data = {'code' : code}, timeout=timeout)
if r.status_code != requests.codes.ok or r.url == URL_CONFIRM: # would have redirected away if success
raise VerificationRequired()
self.save() # Save cookies now for use by command-line app
self.session.cookies.save() # Save cookies now for use by command-line app
self.login()
def query(self):
@ -270,16 +276,14 @@ class Session:
return data
def save(self):
self.session.cookies.save()
def close(self):
self.state = Session.STATE_NONE
try:
self.session.cookies.save()
self.session.close()
except:
pass
if self.session:
try:
self.session.cookies.save()
self.session.close()
except:
if __debug__: print_exc()
self.session = None
def dump(self, r):

View File

@ -1,3 +1,4 @@
import keyring
import numbers
import sys
from os import getenv, makedirs, mkdir, pardir
@ -8,7 +9,7 @@ from sys import platform
appname = 'EDMarketConnector'
applongname = 'E:D Market Connector'
appcmdname = 'EDMC'
appversion = '2.2.5.0'
appversion = '2.2.6.1'
update_feed = 'https://marginal.org.uk/edmarketconnector.xml'
update_interval = 47*60*60
@ -130,12 +131,12 @@ class Config:
if not getattr(sys, 'frozen', False):
# Don't use Python's settings if interactive
self.bundle = 'uk.org.marginal.%s' % appname.lower()
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.bundle
self.identifier = 'uk.org.marginal.%s' % appname.lower()
NSBundle.mainBundle().infoDictionary()['CFBundleIdentifier'] = self.identifier
else:
self.bundle = NSBundle.mainBundle().bundleIdentifier()
self.identifier = NSBundle.mainBundle().bundleIdentifier()
self.defaults = NSUserDefaults.standardUserDefaults()
self.settings = dict(self.defaults.persistentDomainForName_(self.bundle) or {}) # make writeable
self.settings = dict(self.defaults.persistentDomainForName_(self.identifier) or {}) # make writeable
# Check out_dir exists
if not self.get('outdir') or not isdir(self.get('outdir')):
@ -161,7 +162,7 @@ class Config:
self.settings.pop(key, None)
def save(self):
self.defaults.setPersistentDomain_forName_(self.settings, self.bundle)
self.defaults.setPersistentDomain_forName_(self.settings, self.identifier)
self.defaults.synchronize()
def close(self):
@ -188,6 +189,8 @@ class Config:
self.respath = dirname(getattr(sys, 'frozen', False) and sys.executable or __file__)
self.identifier = applongname
self.hkey = HKEY()
disposition = DWORD()
if RegCreateKeyEx(HKEY_CURRENT_USER, r'Software\Marginal\EDMarketConnector', 0, None, 0, KEY_ALL_ACCESS, None, ctypes.byref(self.hkey), ctypes.byref(disposition)):
@ -281,6 +284,8 @@ class Config:
self.respath = dirname(__file__)
self.identifier = 'uk.org.marginal.%s' % appname.lower()
self.filename = join(getenv('XDG_CONFIG_HOME', expanduser('~/.config')), appname, '%s.ini' % appname)
if not isdir(dirname(self.filename)):
makedirs(dirname(self.filename))
@ -298,7 +303,7 @@ class Config:
try:
val = self.config.get(self.SECTION, key)
if u'\n' in val:
return val.split(u'\n')
return val.split(u'\n')[:-1]
else:
return val
except:
@ -314,7 +319,7 @@ class Config:
if isinstance(val, basestring) or isinstance(val, numbers.Integral):
self.config.set(self.SECTION, key, val)
elif hasattr(val, '__iter__'): # iterable
self.config.set(self.SECTION, key, u'\n'.join([unicode(x) for x in val]))
self.config.set(self.SECTION, key, u'\n'.join([unicode(x) for x in val] + [u';']))
else:
raise NotImplementedError()
@ -334,5 +339,19 @@ class Config:
def __init__(self):
raise NotImplementedError('Implement me')
# Common
def get_password(self, account):
return keyring.get_password(self.identifier, account)
def set_password(self, account, password):
keyring.set_password(self.identifier, account, password)
def delete_password(self, account):
try:
keyring.delete_password(self.identifier, account)
except:
pass # don't care - silently fail
# singleton
config = Config()

View File

@ -7,6 +7,7 @@ import urllib2
import Tkinter as tk
from config import appname, applongname, appversion, config
from monitor import monitor
if __debug__:
from traceback import print_exc
@ -79,10 +80,11 @@ class EDSM:
# Call an EDSM endpoint with args (which should be quoted)
def call(self, endpoint, args, check_msgnum=True):
try:
idx = config.get('cmdrs').index(monitor.cmdr)
url = 'https://www.edsm.net/%s?commanderName=%s&apiKey=%s&fromSoftware=%s&fromSoftwareVersion=%s' % (
endpoint,
urllib2.quote(config.get('edsm_cmdrname').encode('utf-8')),
urllib2.quote(config.get('edsm_apikey')),
urllib2.quote(config.get('edsm_usernames')[idx].encode('utf-8')),
urllib2.quote(config.get('edsm_apikeys')[idx]),
urllib2.quote(applongname),
urllib2.quote(appversion),
) + args

View File

@ -106,7 +106,7 @@ class EDLogs(FileSystemEventHandler):
# Latest pre-existing logfile - e.g. if E:D is already running. Assumes logs sort alphabetically.
# Do this before setting up the observer in case the journal directory has gone away
try:
logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')])
logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.') and x.endswith('.log')])
self.logfile = logfiles and join(self.currentdir, logfiles[-1]) or None
except:
self.logfile = None
@ -129,6 +129,8 @@ class EDLogs(FileSystemEventHandler):
print '%s "%s"' % (polling and 'Polling' or 'Monitoring', self.currentdir)
print 'Start logfile "%s"' % self.logfile
self.event_queue.append(None) # Generate null event to signal (re)start
if not self.running():
self.thread = threading.Thread(target = self.worker, name = 'Journal worker')
self.thread.daemon = True
@ -163,7 +165,7 @@ class EDLogs(FileSystemEventHandler):
def on_created(self, event):
# watchdog callback, e.g. client (re)started.
if not event.is_directory and basename(event.src_path).startswith('Journal.'):
if not event.is_directory and basename(event.src_path).startswith('Journal.') and basename(event.src_path).endswith('.log'):
self.logfile = event.src_path
def worker(self):
@ -181,8 +183,6 @@ class EDLogs(FileSystemEventHandler):
except:
if __debug__:
print 'Invalid journal entry "%s"' % repr(line)
self.event_queue.append(None) # Generate null event to signal start
self.root.event_generate('<<JournalEvent>>', when="tail")
else:
loghandle = None
@ -197,7 +197,7 @@ class EDLogs(FileSystemEventHandler):
else:
# Poll
try:
logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.')])
logfiles = sorted([x for x in listdir(self.currentdir) if x.startswith('Journal.') and x.endswith('.log')])
newlogfile = logfiles and join(self.currentdir, logfiles[-1]) or None
except:
if __debug__: print_exc()

159
prefs.py
View File

@ -80,12 +80,13 @@ class PreferencesDialog(tk.Toplevel):
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
self.resizable(tk.FALSE, tk.FALSE)
style = ttk.Style()
self.cmdr = False # Note if Cmdr changes in the Journal
frame = ttk.Frame(self)
frame.grid(sticky=tk.NSEW)
notebook = nb.Notebook(frame)
notebook.bind('<<NotebookTabChanged>>', self.outvarchanged) # Recompute on tab change
PADX = 10
BUTTONX = 12 # indent Checkbuttons and Radiobuttons
@ -96,17 +97,23 @@ class PreferencesDialog(tk.Toplevel):
nb.Label(credframe, text=_('Credentials')).grid(padx=PADX, sticky=tk.W) # Section heading in settings
ttk.Separator(credframe, orient=tk.HORIZONTAL).grid(columnspan=2, padx=PADX, pady=PADY, sticky=tk.EW)
nb.Label(credframe, text=_('Please log in with your Elite: Dangerous account details')).grid(padx=PADX, columnspan=2, sticky=tk.W) # Use same text as E:D Launcher's login dialog
nb.Label(credframe, text=_('Username (Email)')).grid(row=10, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog
nb.Label(credframe, text=_('Password')).grid(row=11, padx=PADX, sticky=tk.W) # Use same text as E:D Launcher's login dialog
self.cred_label = nb.Label(credframe, text=_('Please log in with your Elite: Dangerous account details')) # Use same text as E:D Launcher's login dialog
self.cred_label.grid(padx=PADX, columnspan=2, sticky=tk.W)
self.cmdr_label = nb.Label(credframe, text=_('Cmdr')) # Main window
self.cmdr_label.grid(row=10, padx=PADX, sticky=tk.W)
self.username_label = nb.Label(credframe, text=_('Username (Email)')) # Use same text as E:D Launcher's login dialog
self.username_label.grid(row=11, padx=PADX, sticky=tk.W)
self.password_label = nb.Label(credframe, text=_('Password')) # Use same text as E:D Launcher's login dialog
self.password_label.grid(row=12, padx=PADX, sticky=tk.W)
self.cmdr_text = nb.Label(credframe)
self.cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W)
self.username = nb.Entry(credframe)
self.username.insert(0, config.get('username') or '')
self.username.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
self.username.focus_set()
self.username.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
if not monitor.is_beta and monitor.cmdr:
self.username.focus_set()
self.password = nb.Entry(credframe, show=u'')
self.password.insert(0, config.get('password') or '')
self.password.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
self.password.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
nb.Label(credframe).grid(sticky=tk.W) # big spacer
nb.Label(credframe, text=_('Privacy')).grid(padx=PADX, sticky=tk.W) # Section heading in settings
@ -125,13 +132,17 @@ class PreferencesDialog(tk.Toplevel):
output = config.getint('output') or (config.OUT_MKT_EDDN | config.OUT_SYS_EDDN | config.OUT_SHIP) # default settings
nb.Label(outframe, text=_('Please choose what data to save')).grid(columnspan=2, padx=PADX, sticky=tk.W)
self.out_label = nb.Label(outframe, text=_('Please choose what data to save'))
self.out_label.grid(columnspan=2, padx=PADX, sticky=tk.W)
self.out_csv = tk.IntVar(value = (output & config.OUT_MKT_CSV ) and 1)
nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W)
self.out_csv_button = nb.Checkbutton(outframe, text=_('Market data in CSV format file'), variable=self.out_csv, command=self.outvarchanged)
self.out_csv_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W)
self.out_td = tk.IntVar(value = (output & config.OUT_MKT_TD ) and 1)
nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, sticky=tk.W)
self.out_td_button = nb.Checkbutton(outframe, text=_('Market data in Trade Dangerous format file'), variable=self.out_td, command=self.outvarchanged)
self.out_td_button.grid(columnspan=2, padx=BUTTONX, sticky=tk.W)
self.out_ship= tk.IntVar(value = (output & (config.OUT_SHIP|config.OUT_SHIP_EDS|config.OUT_SHIP_CORIOLIS) and 1))
nb.Checkbutton(outframe, text=_('Ship loadout'), variable=self.out_ship, command=self.outvarchanged).grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W) # Output setting
self.out_ship_button = nb.Checkbutton(outframe, text=_('Ship loadout'), variable=self.out_ship, command=self.outvarchanged) # Output setting
self.out_ship_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
self.out_auto = tk.IntVar(value = 0 if output & config.OUT_MKT_MANUAL else 1) # inverted
self.out_auto_button = nb.Checkbutton(outframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting
self.out_auto_button.grid(columnspan=2, padx=BUTTONX, pady=(5,0), sticky=tk.W)
@ -155,7 +166,8 @@ class PreferencesDialog(tk.Toplevel):
HyperlinkLabel(eddnframe, text='Elite Dangerous Data Network', background=nb.Label().cget('background'), url='https://github.com/jamesremuscat/EDDN/wiki', underline=True).grid(padx=PADX, sticky=tk.W) # Don't translate
self.eddn_station= tk.IntVar(value = (output & config.OUT_MKT_EDDN) and 1)
nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged).grid(padx=BUTTONX, pady=(5,0), sticky=tk.W) # Output setting
self.eddn_station_button = nb.Checkbutton(eddnframe, text=_('Send station data to the Elite Dangerous Data Network'), variable=self.eddn_station, command=self.outvarchanged) # Output setting
self.eddn_station_button.grid(padx=BUTTONX, pady=(5,0), sticky=tk.W)
self.eddn_auto_button = nb.Checkbutton(eddnframe, text=_('Automatically update on docking'), variable=self.out_auto, command=self.outvarchanged) # Output setting
self.eddn_auto_button.grid(padx=BUTTONX, sticky=tk.W)
self.eddn_system = tk.IntVar(value = (output & config.OUT_SYS_EDDN) and 1)
@ -180,17 +192,20 @@ class PreferencesDialog(tk.Toplevel):
self.edsm_label = HyperlinkLabel(edsmframe, text=_('Elite Dangerous Star Map credentials'), background=nb.Label().cget('background'), url='https://www.edsm.net/settings/api', underline=True) # Section heading in settings
self.edsm_label.grid(columnspan=2, padx=PADX, sticky=tk.W)
self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Commander Name')) # EDSM setting
self.edsm_cmdr_label = nb.Label(edsmframe, text=_('Cmdr')) # Main window
self.edsm_cmdr_label.grid(row=10, padx=PADX, sticky=tk.W)
self.edsm_cmdr = nb.Entry(edsmframe)
self.edsm_cmdr.insert(0, config.get('edsm_cmdrname') or '')
self.edsm_cmdr.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
self.edsm_cmdr_text = nb.Label(edsmframe)
self.edsm_cmdr_text.grid(row=10, column=1, padx=PADX, pady=PADY, sticky=tk.W)
self.edsm_user_label = nb.Label(edsmframe, text=_('Commander Name')) # EDSM setting
self.edsm_user_label.grid(row=11, padx=PADX, sticky=tk.W)
self.edsm_user = nb.Entry(edsmframe)
self.edsm_user.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
self.edsm_apikey_label = nb.Label(edsmframe, text=_('API Key')) # EDSM setting
self.edsm_apikey_label.grid(row=11, padx=PADX, sticky=tk.W)
self.edsm_apikey_label.grid(row=12, padx=PADX, sticky=tk.W)
self.edsm_apikey = nb.Entry(edsmframe)
self.edsm_apikey.insert(0, config.get('edsm_apikey') or '')
self.edsm_apikey.grid(row=11, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
self.edsm_apikey.grid(row=12, column=1, padx=PADX, pady=PADY, sticky=tk.EW)
notebook.add(edsmframe, text='EDSM') # Not translated
@ -323,30 +338,59 @@ class PreferencesDialog(tk.Toplevel):
0x10000, None, position):
self.geometry("+%d+%d" % (position.left, position.top))
def outvarchanged(self):
def outvarchanged(self, event=None):
self.cmdr_text['state'] = self.edsm_cmdr_text['state'] = tk.NORMAL # must be writable to update
self.cmdr_text['text'] = self.edsm_cmdr_text['text'] = (monitor.cmdr or _('None')) + (monitor.is_beta and ' [Beta]' or '') # No hotkey/shortcut currently defined
if self.cmdr != monitor.cmdr:
# Cmdr has changed - update settings
self.username['state'] = tk.NORMAL
self.username.delete(0, tk.END)
self.password['state'] = tk.NORMAL
self.password.delete(0, tk.END)
self.edsm_user['state'] = tk.NORMAL
self.edsm_user.delete(0, tk.END)
self.edsm_apikey['state'] = tk.NORMAL
self.edsm_apikey.delete(0, tk.END)
if monitor.cmdr and config.get('cmdrs') and monitor.cmdr in config.get('cmdrs'):
config_idx = config.get('cmdrs').index(monitor.cmdr)
self.username.insert(0, config.get('fdev_usernames')[config_idx] or '')
self.password.insert(0, config.get_password(config.get('fdev_usernames')[config_idx]) or '')
self.edsm_user.insert(0, config.get('edsm_usernames')[config_idx] or '')
self.edsm_apikey.insert(0, config.get('edsm_apikeys')[config_idx] or '')
elif monitor.cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'):
# migration from <= 2.25
self.username.insert(0, config.get('username') or '')
self.password.insert(0, config.get('password') or '')
self.edsm_user.insert(0,config.get('edsm_cmdrname') or '')
self.edsm_apikey.insert(0, config.get('edsm_apikey') or '')
self.cmdr = monitor.cmdr
cmdr_state = not monitor.is_beta and monitor.cmdr and tk.NORMAL or tk.DISABLED
self.cred_label['state'] = self.cmdr_label['state'] = self.username_label['state'] = self.password_label['state'] = cmdr_state
self.cmdr_text['state'] = self.username['state'] = self.password['state'] = cmdr_state
self.displaypath(self.outdir, self.outdir_entry)
self.displaypath(self.logdir, self.logdir_entry)
logdir = self.logdir.get()
logvalid = logdir and exists(logdir)
local = self.out_td.get() or self.out_csv.get() or self.out_ship.get()
self.out_auto_button['state'] = local and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED
self.out_label['state'] = self.out_csv_button['state'] = self.out_td_button['state'] = self.out_ship_button['state'] = not monitor.is_beta and tk.NORMAL or tk.DISABLED
local = not monitor.is_beta and (self.out_td.get() or self.out_csv.get() or self.out_ship.get())
self.out_auto_button['state'] = local and logvalid and tk.NORMAL or tk.DISABLED
self.outdir_label['state'] = local and tk.NORMAL or tk.DISABLED
self.outbutton['state'] = local and tk.NORMAL or tk.DISABLED
self.outdir_entry['state'] = local and 'readonly' or tk.DISABLED
self.eddn_station_button['state'] = not monitor.is_beta and tk.NORMAL or tk.DISABLED
self.eddn_auto_button['state'] = self.eddn_station.get() and logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED
self.eddn_system_button['state']= logvalid and tk.NORMAL or tk.DISABLED
self.eddn_delay_button['state'] = logvalid and eddn.replayfile and self.eddn_system.get() and tk.NORMAL or tk.DISABLED
self.edsm_log_button['state'] = logvalid and tk.NORMAL or tk.DISABLED
edsm_state = logvalid and self.edsm_log.get() and tk.NORMAL or tk.DISABLED
self.edsm_label['state'] = edsm_state
self.edsm_cmdr_label['state'] = edsm_state
self.edsm_apikey_label['state'] = edsm_state
self.edsm_cmdr['state'] = edsm_state
self.edsm_apikey['state'] = edsm_state
self.edsm_log_button['state'] = logvalid and not monitor.is_beta and tk.NORMAL or tk.DISABLED
edsm_state = logvalid and monitor.cmdr and not monitor.is_beta and self.edsm_log.get() and tk.NORMAL or tk.DISABLED
self.edsm_label['state'] = self.edsm_cmdr_label['state'] = self.edsm_user_label['state'] = self.edsm_apikey_label['state'] = edsm_state
self.edsm_cmdr_text['state'] = self.edsm_user['state'] = self.edsm_apikey['state'] = edsm_state
def filebrowse(self, title, pathvar):
if platform != 'win32':
@ -476,9 +520,22 @@ class PreferencesDialog(tk.Toplevel):
def apply(self):
credentials = (config.get('username'), config.get('password'))
config.set('username', self.username.get().strip())
config.set('password', self.password.get().strip())
if self.cmdr and not monitor.is_beta:
if self.password.get().strip():
config.set_password(self.username.get().strip(), self.password.get().strip()) # Can fail if keyring not unlocked
else:
config.delete_password(self.username.get().strip()) # user may have cleared the password field
if not config.get('cmdrs'):
config.set('cmdrs', [self.cmdr])
config.set('fdev_usernames', [self.username.get().strip()])
config.set('edsm_usernames', [self.edsm_user.get().strip()])
config.set('edsm_apikeys', [self.edsm_apikey.get().strip()])
else:
idx = config.get('cmdrs').index(self.cmdr) if self.cmdr in config.get('cmdrs') else -1
_putfirst('cmdrs', idx, self.cmdr)
_putfirst('fdev_usernames', idx, self.username.get().strip())
_putfirst('edsm_usernames', idx, self.edsm_user.get().strip())
_putfirst('edsm_apikeys', idx, self.edsm_apikey.get().strip())
config.set('output',
(self.out_td.get() and config.OUT_MKT_TD) +
@ -491,9 +548,6 @@ class PreferencesDialog(tk.Toplevel):
(self.edsm_log.get() and config.OUT_SYS_EDSM))
config.set('outdir', self.outdir.get().startswith('~') and join(config.home, self.outdir.get()[2:]) or self.outdir.get())
config.set('edsm_cmdrname', self.edsm_cmdr.get().strip())
config.set('edsm_apikey', self.edsm_apikey.get().strip())
logdir = self.logdir.get()
if config.default_journal_dir and logdir.lower() == config.default_journal_dir.lower():
config.set('journaldir', '') # default location
@ -623,3 +677,34 @@ class AuthenticationDialog(tk.Toplevel):
self.parent.wm_attributes('-topmost', config.getint('always_ontop') and 1 or 0)
self.destroy()
if self.callback: self.callback(None)
# migration from <= 2.25. Assumes current Cmdr corresponds to the saved credentials
def migrate(current_cmdr):
if current_cmdr and not config.get('cmdrs') and config.get('username') and config.get('password'):
config.set_password(config.get('username'), config.get('password')) # Can fail on Linux
config.set('cmdrs', [current_cmdr])
config.set('fdev_usernames', [config.get('username')])
config.set('edsm_usernames', [config.get('edsm_cmdrname') or ''])
config.set('edsm_apikeys', [config.get('edsm_apikey') or ''])
# XXX to be done for release
# config.delete('username')
# config.delete('password')
# config.delete('edsm_cmdrname')
# config.delete('edsm_apikey')
# Put current Cmdr first in the lists
def make_current(current_cmdr):
if current_cmdr and config.get('cmdrs') and current_cmdr in config.get('cmdrs'):
idx = config.get('cmdrs').index(current_cmdr)
_putfirst('cmdrs', idx)
_putfirst('fdev_usernames', idx)
_putfirst('edsm_usernames', idx)
_putfirst('edsm_apikeys', idx)
def _putfirst(setting, config_idx, new_value=None):
assert config_idx>=0 or new_value is not None, (setting, config_idx, new_value)
values = config.get(setting)
values.insert(0, new_value if config_idx<0 else values.pop(config_idx))
if new_value is not None:
values[0] = new_value
config.set(setting, values)

View File

@ -1,4 +1,5 @@
argh>=0.26.2
keyring>=3.3
pathtools>=0.1.2
PyYAML>=3.12
requests>=2.11.1

View File

@ -70,7 +70,7 @@ if sys.platform=='darwin':
OPTIONS = { 'py2app':
{'dist_dir': dist_dir,
'optimize': 2,
'packages': [ 'requests' ],
'packages': [ 'requests', 'keyring.backends' ],
'frameworks': [ 'Sparkle.framework' ],
'excludes': [ 'PIL', 'simplejson' ],
'iconfile': '%s.icns' % APPNAME,
@ -100,7 +100,7 @@ elif sys.platform=='win32':
OPTIONS = { 'py2exe':
{'dist_dir': dist_dir,
'optimize': 2,
'packages': [ 'requests' ],
'packages': [ 'requests', 'keyring.backends' ],
'excludes': [ 'PIL', 'simplejson' ],
}
}

View File

@ -11,6 +11,7 @@ import myNotebook as nb
import companion
from companion import ship_map
from monitor import monitor
import prefs
if platform=='win32':
@ -175,6 +176,9 @@ class StatsDialog():
self.showstats()
def showstats(self):
if not monitor.cmdr or monitor.is_beta:
return # In Beta - do nothing
self.status['text'] = _('Fetching data...')
self.parent.update_idletasks()