diff --git a/CHANGELOG.md b/CHANGELOG.md index db5b778..32c9a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ ### New Features: -* Objectives! If you use the API to connect to a server that supports them (API ≥ v1.6.0) then your squadron or group can define shared missions that multiple CMDRs can work towards. Missions can be of various types (for example - `win a war` or `boost a faction`) and each mission can have one or more targets (for example - `win xx space CZs` or `generate yyy CR in trade profit`). The objectives are shown on the overlay in-game if you have it enabled. +* Objectives! If you use the API to connect to a server that supports them (API ≥ v1.6.0) then your squadron or group can define shared missions that multiple CMDRs can work towards. + - Missions can be of various types (for example - `win a war` or `boost a faction`) and each mission can have one or more targets (for example - `win xx space CZs` or `generate yyy CR in trade profit`). + - Objectives are shown in a new window accessible from the main EDMC window - click the 𖦏 button. The layout is a bit basic at the moment, it will probably improve in future. + - If you use the in-game overlay, Objectives are also displayed on a new overlay panel in-game. * Conflict states are highlighted in the activity window: Elections in orange and wars in red. * The individual tick time for each system is now reported on the activity window and on the overlay in-game. diff --git a/assets/button_objectives.png b/assets/button_objectives.png new file mode 100644 index 0000000..8595df7 Binary files /dev/null and b/assets/button_objectives.png differ diff --git a/bgstally/api.py b/bgstally/api.py index 8c4fe93..fd96788 100644 --- a/bgstally/api.py +++ b/bgstally/api.py @@ -71,6 +71,9 @@ def __init__(self, bgstally, data: list = None): # Events queue is used to batch up events API messages. All batched messages are sent when the worker works. self.events_queue: Queue = Queue() + # Received data state (transient, we don't save or load this) + self.objectives: list = [] + self.activities_thread: Thread = Thread(target=self._activities_worker, name=f"BGSTally Activities API Worker ({self.url})") self.activities_thread.daemon = True self.activities_thread.start() @@ -338,7 +341,8 @@ def _objectives_received(self, success: bool, response: Response, request: BGSTa Debug.logger.warning(f"Objectives data is invalid (not a list)") return - self.bgstally.objectives_manager.set_objectives(objectives_data) + self.objectives = objectives_data + self.bgstally.objectives_manager.objectives_received(self) def _get_headers(self) -> dict: diff --git a/bgstally/objectivesmanager.py b/bgstally/objectivesmanager.py index 4db24e1..928b05c 100644 --- a/bgstally/objectivesmanager.py +++ b/bgstally/objectivesmanager.py @@ -33,12 +33,25 @@ class MissionTargetType(str, Enum): class ObjectivesManager: """ - Handles the management of objectives + Handles the management of objectives. + + Note that the objectives are stored inside the API object and there is only a single API tracked here. So, if multiple APIs + are implemented in future, it will flip-flop between them and we either need to handle that or limit objectives to a single API. """ def __init__(self, bgstally): self.bgstally = bgstally - self._objectives: list[dict] = [] + self.api: API = None + + + def objectives_available(self) -> bool: + """Check whether any objectives are available + + Returns: + bool: True if there are objectives + """ + if self.api is None: return False + else: return len(self.api.objectives) > 0 def get_objectives(self) -> list: @@ -47,16 +60,22 @@ def get_objectives(self) -> list: Returns: list: current list of objectives """ - return self._objectives + if self.api is None: return [] + else: return self.api.objectives - def set_objectives(self, objectives: list): - """Set the current objectives + def objectives_received(self, api: API): + """Objectives have been received from the API Args: - objectives (dict): The list of objectives + api (API): The API object """ - self._objectives = objectives + previous_available: bool = self.objectives_available() + self.api = api + + if previous_available != self.objectives_available(): + # We've flipped from having objectives to not having objectives or vice versa. Refresh the plugin frame. + self.bgstally.ui.frame.after(1000, self.bgstally.ui.update_plugin_frame()) def get_human_readable_objectives(self) -> str: @@ -66,8 +85,9 @@ def get_human_readable_objectives(self) -> str: str: The human readable objectives """ result: str = "" + if self.api is None: return result - for mission in self._objectives: + for mission in self.api.objectives: mission_title: str|None = mission.get('title') mission_description: str|None = mission.get('description') mission_system: str|None = mission.get('system') diff --git a/bgstally/ui.py b/bgstally/ui.py index 9cb3d39..35d4394 100644 --- a/bgstally/ui.py +++ b/bgstally/ui.py @@ -21,6 +21,7 @@ from bgstally.windows.cmdrs import WindowCMDRs from bgstally.windows.fleetcarrier import WindowFleetCarrier from bgstally.windows.legend import WindowLegend +from bgstally.windows.objectives import WindowObjectives from config import config from thirdparty.tksheet import Sheet from thirdparty.Tooltip import ToolTip @@ -50,6 +51,7 @@ def __init__(self, bgstally): self.image_button_dropdown_menu = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_dropdown_menu.png")) self.image_button_cmdrs = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_cmdrs.png")) self.image_button_carrier = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_carrier.png")) + self.image_button_objectives = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "button_objectives.png")) self.image_icon_green_tick = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "icon_green_tick_16x16.png")) self.image_icon_red_cross = PhotoImage(file = path.join(self.bgstally.plugin_dir, FOLDER_ASSETS, "icon_red_cross_16x16.png")) @@ -62,6 +64,8 @@ def __init__(self, bgstally): self.window_cmdrs:WindowCMDRs = WindowCMDRs(self.bgstally) self.window_fc:WindowFleetCarrier = WindowFleetCarrier(self.bgstally) self.window_legend:WindowLegend = WindowLegend(self.bgstally) + self.window_objectives:WindowObjectives = WindowObjectives(self.bgstally) + # TODO: When we support multiple APIs, this will no longer be a single instance window self.window_api:WindowAPI = WindowAPI(self.bgstally, self.bgstally.api_manager.apis[0]) @@ -85,34 +89,48 @@ def get_plugin_frame(self, parent_frame: tk.Frame) -> tk.Frame: """ self.frame: tk.Frame = tk.Frame(parent_frame) + column_count: int = 3 + if self.bgstally.capi_fleetcarrier_available(): column_count += 1 + current_row: int = 0 tk.Label(self.frame, image=self.image_logo_bgstally_100).grid(row=current_row, column=0, rowspan=3, sticky=tk.W) self.lbl_version: HyperlinkLabel = HyperlinkLabel(self.frame, text=f"v{str(self.bgstally.version)}", background=nb.Label().cget('background'), url=URL_LATEST_RELEASE, underline=True) - self.lbl_version.grid(row=current_row, column=1, columnspan=3 if self.bgstally.capi_fleetcarrier_available() else 2, sticky=tk.W) + self.lbl_version.grid(row=current_row, column=1, columnspan=column_count, sticky=tk.W) current_row += 1 frm_status: tk.Frame = tk.Frame(self.frame) - frm_status.grid(row=current_row, column=1, columnspan=3 if self.bgstally.capi_fleetcarrier_available() else 2, sticky=tk.W) + frm_status.grid(row=current_row, column=1, columnspan=column_count, sticky=tk.W) self.lbl_status: tk.Label = tk.Label(frm_status, text=_("{plugin_name} Status:").format(plugin_name=self.bgstally.plugin_name)) # LANG: Main window label self.lbl_status.pack(side=tk.LEFT) self.lbl_active: tk.Label = tk.Label(frm_status, width=SIZE_STATUS_ICON_PIXELS, height=SIZE_STATUS_ICON_PIXELS, image=self.image_icon_green_tick if self.bgstally.state.Status.get() == CheckStates.STATE_ON else self.image_icon_red_cross) self.lbl_active.pack(side=tk.LEFT) current_row += 1 self.lbl_tick: tk.Label = tk.Label(self.frame, text=_("Last BGS Tick:") + " " + self.bgstally.tick.get_formatted()) # LANG: Main window label - self.lbl_tick.grid(row=current_row, column=1, columnspan=3 if self.bgstally.capi_fleetcarrier_available() else 2, sticky=tk.W) + self.lbl_tick.grid(row=current_row, column=1, columnspan=column_count, sticky=tk.W) current_row += 1 + current_column: int = 0 self.btn_latest_tick: tk.Button = tk.Button(self.frame, text=_("Latest BGS Tally"), height=SIZE_BUTTON_PIXELS-2, image=self.image_blank, compound=tk.RIGHT, command=partial(self._show_activity_window, self.bgstally.activity_manager.get_current_activity())) # LANG: Button label - self.btn_latest_tick.grid(row=current_row, column=0, padx=3) + self.btn_latest_tick.grid(row=current_row, column=current_column, padx=3) + current_column += 1 self.btn_previous_ticks: tk.Button = tk.Button(self.frame, text=_("Previous BGS Tallies") + " ", height=SIZE_BUTTON_PIXELS-2, image=self.image_button_dropdown_menu, compound=tk.RIGHT, command=self._previous_ticks_popup) # LANG: Button label - self.btn_previous_ticks.grid(row=current_row, column=1, padx=3, sticky=tk.W) + self.btn_previous_ticks.grid(row=current_row, column=current_column, padx=3, sticky=tk.W) + current_column += 1 self.btn_cmdrs: tk.Button = tk.Button(self.frame, image=self.image_button_cmdrs, height=SIZE_BUTTON_PIXELS, width=SIZE_BUTTON_PIXELS, command=self._show_cmdr_list_window) - self.btn_cmdrs.grid(row=current_row, column=2, padx=3) + self.btn_cmdrs.grid(row=current_row, column=current_column, padx=3) + current_column += 1 ToolTip(self.btn_cmdrs, text=_("Show CMDR information window")) # LANG: Main window tooltip if self.bgstally.capi_fleetcarrier_available(): self.btn_carrier: tk.Button = tk.Button(self.frame, image=self.image_button_carrier, state=('normal' if self.bgstally.fleet_carrier.available() else 'disabled'), height=SIZE_BUTTON_PIXELS, width=SIZE_BUTTON_PIXELS, command=self._show_fc_window) - self.btn_carrier.grid(row=current_row, column=3, padx=3) + self.btn_carrier.grid(row=current_row, column=current_column, padx=3) ToolTip(self.btn_carrier, text=_("Show fleet carrier window")) # LANG: Main window tooltip + current_column += 1 else: self.btn_carrier: tk.Button = None + + self.btn_objectives: tk.Button = tk.Button(self.frame, image=self.image_button_objectives, state=('normal' if self.bgstally.objectives_manager.objectives_available() else 'disabled'), height=SIZE_BUTTON_PIXELS, width=SIZE_BUTTON_PIXELS, command=self._show_objectives_window) + self.btn_objectives.grid(row=current_row, column=current_column, padx=3) + ToolTip(self.btn_objectives, text=_("Show objectives / missions window")) # LANG: Main window tooltip + current_column += 1 + current_row += 1 return self.frame @@ -137,6 +155,7 @@ def update_plugin_frame(self): self.btn_latest_tick.config(command=partial(self._show_activity_window, self.bgstally.activity_manager.get_current_activity())) if self.btn_carrier is not None: self.btn_carrier.config(state=('normal' if self.bgstally.fleet_carrier.available() else 'disabled')) + self.btn_objectives.config(state=('normal' if self.bgstally.objectives_manager.objectives_available() else 'disabled')) def get_prefs_frame(self, parent_frame: tk.Frame): @@ -489,6 +508,12 @@ def _show_fc_window(self): self.window_fc.show() + def _show_objectives_window(self): + """Display the Objectives Window + """ + self.window_objectives.show() + + def _show_api_window(self, parent_frame:tk.Frame): """ Display the API configuration window diff --git a/bgstally/windows/objectives.py b/bgstally/windows/objectives.py new file mode 100644 index 0000000..a92e0d2 --- /dev/null +++ b/bgstally/windows/objectives.py @@ -0,0 +1,66 @@ +import tkinter as tk +from tkinter import ttk + +from bgstally.constants import COLOUR_HEADING_1, FONT_HEADING_1, FONT_TEXT +from bgstally.debug import Debug +from bgstally.utils import _, __ +from bgstally.widgets import TextPlus +from config import config +from thirdparty.colors import * + + +class WindowObjectives: + """ + Handles the Objectives window + """ + + def __init__(self, bgstally): + self.bgstally = bgstally + + self.toplevel: tk.Toplevel = None + + + def show(self): + """ + Show our window + """ + if self.toplevel is not None and self.toplevel.winfo_exists(): + self.toplevel.lift() + return + + self.toplevel = tk.Toplevel(self.bgstally.ui.frame) + self.toplevel.title(_("{plugin_name} - Objectives").format(plugin_name=self.bgstally.plugin_name, )) # LANG: Objectives window title + self.toplevel.iconphoto(False, self.bgstally.ui.image_logo_bgstally_32, self.bgstally.ui.image_logo_bgstally_16) + self.toplevel.geometry("800x800") + + frm_container: ttk.Frame = ttk.Frame(self.toplevel) + frm_container.pack(fill=tk.BOTH, expand=True) + + ttk.Label(frm_container, text=_("Objectives from {server_name}".format(server_name=self.bgstally.objectives_manager.api.name)), font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).pack(anchor=tk.NW) # LANG: Label on objectives window + + frm_items: ttk.Frame = ttk.Frame(frm_container) + frm_items.pack(fill=tk.BOTH, padx=5, pady=5, expand=True) + + current_row: int = 0 + + self.txt_objectives: TextPlus = TextPlus(frm_items, wrap=tk.WORD, height=1, font=FONT_TEXT) + sb_objectives: tk.Scrollbar = tk.Scrollbar(frm_items, orient=tk.VERTICAL, command=self.txt_objectives.yview) + self.txt_objectives['yscrollcommand'] = sb_objectives.set + sb_objectives.pack(fill=tk.Y, side=tk.RIGHT) + self.txt_objectives.pack(fill=tk.BOTH, side=tk.LEFT, expand=True) + + self.txt_objectives.insert(tk.INSERT, self.bgstally.objectives_manager.get_human_readable_objectives()) + self.txt_objectives.configure(state='disabled') + + self.toplevel.after(5000, self._update_objectives) + + + def _update_objectives(self): + """Refresh the objectives + """ + self.txt_objectives.configure(state=tk.NORMAL) + self.txt_objectives.delete('1.0', 'end-1c') + self.txt_objectives.insert(tk.INSERT, self.bgstally.objectives_manager.get_human_readable_objectives()) + self.txt_objectives.configure(state=tk.DISABLED) + + self.toplevel.after(5000, self._update_objectives)