1
0
mirror of https://github.com/EDCD/EDMarketConnector.git synced 2025-04-16 09:10:35 +03:00
Athanasius 1dcefafc73
CAPI: Pass tk_response_event in Request to optionally use in response
EDMC.py will also need to make CAPI queries using the new threaded
method, but it has no tkinter, thus we need to pass in the event name
when we expect one to be generated, and only generate it if this is not
None.
2021-08-24 12:41:02 +01:00

482 lines
19 KiB
Python

"""CMDR Status information."""
import csv
import tkinter
import tkinter as tk
from sys import platform
from tkinter import ttk
from typing import TYPE_CHECKING, Any, AnyStr, Callable, Dict, List, NamedTuple, Optional, Sequence, cast
import companion
import EDMCLogging
import myNotebook as nb # noqa: N813
from edmc_data import ship_name_map
from l10n import Locale
from monitor import monitor
logger = EDMCLogging.get_main_logger()
if TYPE_CHECKING:
def _(x: str) -> str: ...
if platform == 'win32':
import ctypes
from ctypes.wintypes import HWND, POINT, RECT, SIZE, UINT
if TYPE_CHECKING:
import ctypes.windll # type: ignore # Fake this into existing, its really a magic dll thing
try:
CalculatePopupWindowPosition = ctypes.windll.user32.CalculatePopupWindowPosition
CalculatePopupWindowPosition.argtypes = [
ctypes.POINTER(POINT), ctypes.POINTER(SIZE), UINT, ctypes.POINTER(RECT), ctypes.POINTER(RECT)
]
GetParent = ctypes.windll.user32.GetParent
GetParent.argtypes = [HWND]
GetWindowRect = ctypes.windll.user32.GetWindowRect
GetWindowRect.argtypes = [HWND, ctypes.POINTER(RECT)]
except Exception: # Not supported under Wine 4.0
CalculatePopupWindowPosition = None # type: ignore
def status(data: Dict[str, Any]) -> List[List[str]]:
"""
Get the current status of the cmdr referred to by data.
:param data: Data to generate status from
:return: Status information about the given cmdr
"""
# StatsResults assumes these three things are first
res = [
[_('Cmdr'), data['commander']['name']], # LANG: Cmdr stats
[_('Balance'), str(data['commander'].get('credits', 0))], # LANG: Cmdr stats
[_('Loan'), str(data['commander'].get('debt', 0))], # LANG: Cmdr stats
]
RANKS = [ # noqa: N806 # Its a constant, just needs to be updated at runtime
# in output order
(_('Combat'), 'combat'), # LANG: Ranking
(_('Trade'), 'trade'), # LANG: Ranking
(_('Explorer'), 'explore'), # LANG: Ranking
(_('CQC'), 'cqc'), # LANG: Ranking
(_('Federation'), 'federation'), # LANG: Ranking
(_('Empire'), 'empire'), # LANG: Ranking
(_('Powerplay'), 'power'), # LANG: Ranking
# ??? , 'crime'), # LANG: Ranking
# ??? , 'service'), # LANG: Ranking
]
RANK_NAMES = { # noqa: N806 # Its a constant, just needs to be updated at runtime
# http://elite-dangerous.wikia.com/wiki/Pilots_Federation#Ranks
'combat': [
_('Harmless'), # LANG: Combat rank
_('Mostly Harmless'), # LANG: Combat rank
_('Novice'), # LANG: Combat rank
_('Competent'), # LANG: Combat rank
_('Expert'), # LANG: Combat rank
_('Master'), # LANG: Combat rank
_('Dangerous'), # LANG: Combat rank
_('Deadly'), # LANG: Combat rank
_('Elite'), # LANG: Top rank
],
'trade': [
_('Penniless'), # LANG: Trade rank
_('Mostly Penniless'), # LANG: Trade rank
_('Peddler'), # LANG: Trade rank
_('Dealer'), # LANG: Trade rank
_('Merchant'), # LANG: Trade rank
_('Broker'), # LANG: Trade rank
_('Entrepreneur'), # LANG: Trade rank
_('Tycoon'), # LANG: Trade rank
_('Elite') # LANG: Top rank
],
'explore': [
_('Aimless'), # LANG: Explorer rank
_('Mostly Aimless'), # LANG: Explorer rank
_('Scout'), # LANG: Explorer rank
_('Surveyor'), # LANG: Explorer rank
_('Trailblazer'), # LANG: Explorer rank
_('Pathfinder'), # LANG: Explorer rank
_('Ranger'), # LANG: Explorer rank
_('Pioneer'), # LANG: Explorer rank
_('Elite') # LANG: Top rank
],
'cqc': [
_('Helpless'), # LANG: CQC rank
_('Mostly Helpless'), # LANG: CQC rank
_('Amateur'), # LANG: CQC rank
_('Semi Professional'), # LANG: CQC rank
_('Professional'), # LANG: CQC rank
_('Champion'), # LANG: CQC rank
_('Hero'), # LANG: CQC rank
_('Gladiator'), # LANG: CQC rank
_('Elite') # LANG: Top rank
],
# http://elite-dangerous.wikia.com/wiki/Federation#Ranks
'federation': [
_('None'), # LANG: No rank
_('Recruit'), # LANG: Federation rank
_('Cadet'), # LANG: Federation rank
_('Midshipman'), # LANG: Federation rank
_('Petty Officer'), # LANG: Federation rank
_('Chief Petty Officer'), # LANG: Federation rank
_('Warrant Officer'), # LANG: Federation rank
_('Ensign'), # LANG: Federation rank
_('Lieutenant'), # LANG: Federation rank
_('Lieutenant Commander'), # LANG: Federation rank
_('Post Commander'), # LANG: Federation rank
_('Post Captain'), # LANG: Federation rank
_('Rear Admiral'), # LANG: Federation rank
_('Vice Admiral'), # LANG: Federation rank
_('Admiral') # LANG: Federation rank
],
# http://elite-dangerous.wikia.com/wiki/Empire#Ranks
'empire': [
_('None'), # LANG: No rank
_('Outsider'), # LANG: Empire rank
_('Serf'), # LANG: Empire rank
_('Master'), # LANG: Empire rank
_('Squire'), # LANG: Empire rank
_('Knight'), # LANG: Empire rank
_('Lord'), # LANG: Empire rank
_('Baron'), # LANG: Empire rank
_('Viscount'), # LANG: Empire rank
_('Count'), # LANG: Empire rank
_('Earl'), # LANG: Empire rank
_('Marquis'), # LANG: Empire rank
_('Duke'), # LANG: Empire rank
_('Prince'), # LANG: Empire rank
_('King') # LANG: Empire rank
],
# http://elite-dangerous.wikia.com/wiki/Ratings
'power': [
_('None'), # LANG: No rank
_('Rating 1'), # LANG: Power rank
_('Rating 2'), # LANG: Power rank
_('Rating 3'), # LANG: Power rank
_('Rating 4'), # LANG: Power rank
_('Rating 5') # LANG: Power rank
],
}
ranks = data['commander'].get('rank', {})
for title, thing in RANKS:
rank = ranks.get(thing)
names = RANK_NAMES[thing]
if isinstance(rank, int):
res.append([title, names[rank] if rank < len(names) else f'Rank {rank}'])
else:
res.append([title, _('None')]) # LANG: No rank
return res
def export_status(data: Dict[str, Any], filename: AnyStr) -> None:
"""
Export status data to a CSV file.
:param data: The data to generate the file from
:param filename: The target file
"""
with open(filename, 'w') as f:
h = csv.writer(f)
h.writerow(('Category', 'Value'))
for thing in status(data):
h.writerow(list(thing))
class ShipRet(NamedTuple):
"""ShipRet is a NamedTuple containing the return data from stats.ships."""
id: str
type: str
name: str
system: str
station: str
value: str
def ships(companion_data: Dict[str, Any]) -> List[ShipRet]:
"""
Return a list of 5 tuples of ship information.
:param data: [description]
:return: A 5 tuple of strings containing: Ship ID, Ship Type Name (internal), Ship Name, System, Station, and Value
"""
ships: List[Dict[str, Any]] = companion.listify(cast(list, companion_data.get('ships')))
current = companion_data['commander'].get('currentShipId')
if isinstance(current, int) and current < len(ships) and ships[current]:
ships.insert(0, ships.pop(current)) # Put current ship first
if not companion_data['commander'].get('docked'):
out: List[ShipRet] = []
# Set current system, not last docked
out.append(ShipRet(
id=str(ships[0]['id']),
type=ship_name_map.get(ships[0]['name'].lower(), ships[0]['name']),
name=str(ships[0].get('shipName', '')),
system=companion_data['lastSystem']['name'],
station='',
value=str(ships[0]['value']['total'])
))
out.extend(
ShipRet(
id=str(ship['id']),
type=ship_name_map.get(ship['name'].lower(), ship['name']),
name=ship.get('shipName', ''),
system=ship['starsystem']['name'],
station=ship['station']['name'],
value=str(ship['value']['total'])
) for ship in ships[1:] if ship
)
return out
return [
ShipRet(
id=str(ship['id']),
type=ship_name_map.get(ship['name'].lower(), ship['name']),
name=ship.get('shipName', ''),
system=ship['starsystem']['name'],
station=ship['station']['name'],
value=str(ship['value']['total'])
) for ship in ships if ship is not None
]
def export_ships(companion_data: Dict[str, Any], filename: AnyStr) -> None:
"""
Export the current ships to a CSV file.
:param companion_data: Data from which to generate the ship list
:param filename: The target file
"""
with open(filename, 'w') as f:
h = csv.writer(f)
h.writerow(['Id', 'Ship', 'Name', 'System', 'Station', 'Value'])
for thing in ships(companion_data):
h.writerow(list(thing))
class StatsDialog():
"""Status dialog containing all of the current cmdr's stats."""
def __init__(self, parent: tk.Tk, status: tk.Label) -> None:
self.parent: tk.Tk = parent
self.status = status
self.showstats()
def showstats(self) -> None:
"""Show the status window for the current cmdr."""
if not monitor.cmdr:
return
# TODO: This needs to use cached data
# LANG: Fetching data from Frontier CAPI in order to display on File > Status
self.status['text'] = _('Fetching data...')
self.parent.update_idletasks()
try:
# TODO: This should use cached data
data = companion.session.profile()
except companion.ServerError as e:
self.status['text'] = str(e)
return
except Exception as e:
logger.exception("error while attempting to show status")
self.status['text'] = str(e)
return
if not data.get('commander') or not data['commander'].get('name', '').strip():
# Shouldn't happen
# LANG: Unknown commander
self.status['text'] = _("Who are you?!")
elif (
not data.get('lastSystem')
or not data['lastSystem'].get('name', '').strip()
or not data.get('lastStarport')
or not data['lastStarport'].get('name', '').strip()
):
# Shouldn't happen
# LANG: Unknown location
self.status['text'] = _("Where are you?!")
elif not data.get('ship') or not data['ship'].get('modules') or not data['ship'].get('name', '').strip():
# Shouldn't happen
# LANG: Unknown ship
self.status['text'] = _("What are you flying?!")
else:
self.status['text'] = ''
StatsResults(self.parent, data)
class StatsResults(tk.Toplevel):
"""Status window."""
def __init__(self, parent: tk.Tk, data: Dict[str, Any]) -> None:
tk.Toplevel.__init__(self, parent)
self.parent = parent
stats = status(data)
self.title(' '.join(stats[0])) # assumes first thing is player name
if parent.winfo_viewable():
self.transient(parent)
# position over parent
if platform != 'darwin' or parent.winfo_rooty() > 0: # http://core.tcl.tk/tk/tktview/c84f660833546b1b84e7
self.geometry(f"+{parent.winfo_rootx()}+{parent.winfo_rooty()}")
# remove decoration
self.resizable(tk.FALSE, tk.FALSE)
if platform == 'win32':
self.attributes('-toolwindow', tk.TRUE)
elif platform == 'darwin':
# http://wiki.tcl.tk/13428
parent.call('tk::unsupported::MacWindowStyle', 'style', self, 'utility')
frame = ttk.Frame(self)
frame.grid(sticky=tk.NSEW)
notebook = nb.Notebook(frame)
page = self.addpage(notebook)
for thing in stats[1:3]:
# assumes things two and three are money
self.addpagerow(page, [thing[0], self.credits(int(thing[1]))], with_copy=True)
for thing in stats[3:]:
self.addpagerow(page, thing, with_copy=True)
ttk.Frame(page).grid(pady=5) # bottom spacer
notebook.add(page, text=_('Status')) # LANG: Status dialog title
page = self.addpage(notebook, [
_('Ship'), # LANG: Status dialog subtitle
'',
_('System'), # LANG: Main window
_('Station'), # LANG: Status dialog subtitle
_('Value'), # LANG: Status dialog subtitle - CR value of ship
])
shiplist = ships(data)
for ship_data in shiplist:
# skip id, last item is money
self.addpagerow(page, list(ship_data[1:-1]) + [self.credits(int(ship_data[-1]))], with_copy=True)
ttk.Frame(page).grid(pady=5) # bottom spacer
notebook.add(page, text=_('Ships')) # LANG: Status dialog title
if platform != 'darwin':
buttonframe = ttk.Frame(frame)
buttonframe.grid(padx=10, pady=(0, 10), sticky=tk.NSEW) # type: ignore # the tuple is supported
buttonframe.columnconfigure(0, weight=1)
ttk.Label(buttonframe).grid(row=0, column=0) # spacer
ttk.Button(buttonframe, text='OK', command=self.destroy).grid(row=0, column=1, sticky=tk.E)
# wait for window to appear on screen before calling grab_set
self.wait_visibility()
self.grab_set()
# Ensure fully on-screen
if platform == 'win32' and CalculatePopupWindowPosition:
position = RECT()
GetWindowRect(GetParent(self.winfo_id()), position)
if CalculatePopupWindowPosition(
POINT(parent.winfo_rootx(), parent.winfo_rooty()),
# - is evidently supported on the C side
SIZE(position.right - position.left, position.bottom - position.top), # type: ignore
0x10000, None, position
):
self.geometry(f"+{position.left}+{position.top}")
def addpage(self, parent, header: List[str] = None, align: Optional[str] = None) -> tk.Frame:
"""
Add a page to the StatsResults screen.
:param parent: The parent widget to put this under
:param header: The headers for the table, defaults to an empty list
:param align: Alignment to use for this page, defaults to None
:return: The Frame that was created
"""
if header is None:
header = []
page = nb.Frame(parent)
page.grid(pady=10, sticky=tk.NSEW)
page.columnconfigure(0, weight=1)
if header:
self.addpageheader(page, header, align=align)
return page
def addpageheader(self, parent: tk.Frame, header: Sequence[str], align: Optional[str] = None) -> None:
"""
Add the column headers to the page, followed by a separator.
:param parent: The parent widget to add this to
:param header: The headers to add to the page
:param align: The alignment of the page, defaults to None
"""
self.addpagerow(parent, header, align=align, with_copy=False)
ttk.Separator(parent, orient=tk.HORIZONTAL).grid(columnspan=len(header), padx=10, pady=2, sticky=tk.EW)
def addpagespacer(self, parent) -> None:
"""Add a spacer to the page."""
self.addpagerow(parent, [''])
def addpagerow(
self, parent: tk.Frame, content: Sequence[str], align: Optional[str] = None, with_copy: bool = False
):
"""
Add a single row to parent.
:param parent: The widget to add the data to
:param content: The columns of the row to add
:param align: The alignment of the data, defaults to tk.W
"""
row = -1 # To silence unbound warnings
for i in range(len(content)):
# label = HyperlinkLabel(parent, text=content[i], popup_copy=True)
label = nb.Label(parent, text=content[i])
if with_copy:
label.bind('<Button-1>', self.copy_callback(label, content[i]))
if i == 0:
label.grid(padx=10, sticky=tk.W)
row = parent.grid_size()[1]-1
elif align is None and i == len(content) - 1: # Assumes last column right justified if unspecified
label.grid(row=row, column=i, padx=10, sticky=tk.E)
else:
label.grid(row=row, column=i, padx=10, sticky=align or tk.W)
def credits(self, value: int) -> str:
"""Localised string of given int, including a trailing ` Cr`."""
# TODO: Locale is a class, this calls an instance method on it with an int as its `self`
return Locale.string_from_number(value, 0) + ' Cr' # type: ignore
@staticmethod
def copy_callback(label: tk.Label, text_to_copy: str) -> Callable[..., None]:
"""Copy data in Label to clipboard."""
def do_copy(event: tkinter.Event) -> None:
label.clipboard_clear()
label.clipboard_append(text_to_copy)
old_bg = label['bg']
label['bg'] = 'gray49'
label.after(100, (lambda: label.configure(bg=old_bg)))
return do_copy