From 604bbef81cd222dec502261f8709c7c23e4a5870 Mon Sep 17 00:00:00 2001 From: aussig Date: Fri, 27 Sep 2024 12:50:06 +0100 Subject: [PATCH 01/27] Add new section to CHANGELOG --- CHANGELOG.md | 5 +++++ load.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ebe819..a0fa912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## vx.x.x - xxxx-xx-xx + + + + ## v4.1.1 - 2024-09-27 ### Bug Fixes: diff --git a/load.py b/load.py index 0202f7d..e20b900 100644 --- a/load.py +++ b/load.py @@ -9,7 +9,7 @@ from bgstally.debug import Debug PLUGIN_NAME = "BGS-Tally" -PLUGIN_VERSION = semantic_version.Version.coerce("4.1.1") +PLUGIN_VERSION = semantic_version.Version.coerce("4.2.0-dev") # Initialise the main plugin class bgstally.globals.this = this = BGSTally(PLUGIN_NAME, PLUGIN_VERSION) From 3685041281db033644ccc43c200b295119679444 Mon Sep 17 00:00:00 2001 From: aussig Date: Sat, 28 Sep 2024 10:31:21 +0100 Subject: [PATCH 02/27] Update tksheet to v7.2.15 --- thirdparty/tksheet/__init__.py | 3 +- thirdparty/tksheet/column_headers.py | 97 +++++----- thirdparty/tksheet/functions.py | 79 +++++++- thirdparty/tksheet/main_table.py | 185 +++++++++--------- thirdparty/tksheet/other_classes.py | 75 ++++++-- thirdparty/tksheet/row_index.py | 231 ++++++++++++----------- thirdparty/tksheet/sheet.py | 149 ++++++++++----- thirdparty/tksheet/sheet_options.py | 47 ++--- thirdparty/tksheet/text_editor.py | 17 +- thirdparty/tksheet/themes.py | 8 +- thirdparty/tksheet/top_left_rectangle.py | 19 +- 11 files changed, 566 insertions(+), 344 deletions(-) diff --git a/thirdparty/tksheet/__init__.py b/thirdparty/tksheet/__init__.py index cb0acf9..58e51c1 100644 --- a/thirdparty/tksheet/__init__.py +++ b/thirdparty/tksheet/__init__.py @@ -4,7 +4,7 @@ tksheet - A Python tkinter table widget """ -__version__ = "7.2.9" +__version__ = "7.2.15" from .colors import ( color_map, @@ -37,6 +37,7 @@ alpha2num, consecutive_chunks, consecutive_ranges, + convert_align, data_to_displayed_idxs, displayed_to_data_idxs, dropdown_search_function, diff --git a/thirdparty/tksheet/column_headers.py b/thirdparty/tksheet/column_headers.py index 5eb2b97..f5255a5 100644 --- a/thirdparty/tksheet/column_headers.py +++ b/thirdparty/tksheet/column_headers.py @@ -128,7 +128,6 @@ def __init__(self, *args, **kwargs): self.hidd_checkbox = {} self.hidd_boxes = set() - self.default_header = kwargs["default_header"].lower() self.align = kwargs["header_align"] self.basic_bindings() @@ -481,7 +480,6 @@ def b1_press(self, event: object) -> None: if y < self.MT.min_header_height: y = int(self.MT.min_header_height) self.new_col_height = y - self.create_resize_line(x1, y, x2, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl") elif self.MT.identify_col(x=event.x, allow_end=False) is None: self.MT.deselect("all") elif self.col_selection_enabled and self.rsz_w is None and self.rsz_h is None: @@ -540,22 +538,21 @@ def b1_motion(self, event: object) -> None: fill=self.PAR.ops.resizing_line_fg, tag="rwl2", ) + self.drag_width_resize() elif self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: evy = event.y - self.hide_resize_and_ctrl_lines(ctrl_lines=False) if evy > self.current_height: y = self.MT.canvasy(evy - self.current_height) if evy > self.MT.max_header_height: evy = int(self.MT.max_header_height) y = self.MT.canvasy(evy - self.current_height) self.new_col_height = evy - self.MT.create_resize_line(x1, y, x2, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl") else: y = evy if y < self.MT.min_header_height: y = int(self.MT.min_header_height) self.new_col_height = y - self.create_resize_line(x1, y, x2, y, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rhl") + self.drag_height_resize() elif ( self.drag_and_drop_enabled and self.col_selection_enabled @@ -602,6 +599,10 @@ def b1_motion(self, event: object) -> None: self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=False) try_binding(self.extra_b1_motion_func, event) + def drag_height_resize(self) -> None: + self.set_height(self.new_col_height, set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + def get_b1_motion_box(self, start_col: int, end_col: int) -> tuple[int, int, int, int, Literal["columns"]]: if end_col >= start_col: return 0, start_col, len(self.MT.row_positions) - 1, end_col + 1, "columns" @@ -779,6 +780,32 @@ def event_over_checkbox(self, c: int, datacn: int, event: object, canvasx: float return True return False + def drag_width_resize(self) -> None: + new_col_pos = int(self.coords("rwl")[0]) + old_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] + size = new_col_pos - self.MT.col_positions[self.rsz_w - 1] + if size < self.MT.min_column_width: + new_col_pos = ceil(self.MT.col_positions[self.rsz_w - 1] + self.MT.min_column_width) + elif size > self.MT.max_column_width: + new_col_pos = floor(self.MT.col_positions[self.rsz_w - 1] + self.MT.max_column_width) + increment = new_col_pos - self.MT.col_positions[self.rsz_w] + self.MT.col_positions[self.rsz_w + 1 :] = [ + e + increment for e in islice(self.MT.col_positions, self.rsz_w + 1, None) + ] + self.MT.col_positions[self.rsz_w] = new_col_pos + new_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] + self.MT.allow_auto_resize_columns = False + self.MT.recreate_all_selection_boxes() + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.column_width_resize_func is not None and old_width != new_width: + self.column_width_resize_func( + event_dict( + name="resize", + sheet=self.PAR.name, + resized_columns={self.rsz_w - 1: {"old_size": old_width, "new_size": new_width}}, + ) + ) + def b1_release(self, event: object) -> None: if self.being_drawn_item is not None and (to_sel := self.MT.coords_and_type(self.being_drawn_item)): r_to_sel, c_to_sel = self.MT.selected.row, self.MT.selected.column @@ -795,37 +822,12 @@ def b1_release(self, event: object) -> None: self.being_drawn_item = None self.MT.bind("", self.MT.mousewheel) if self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: + self.drag_width_resize() self.currently_resizing_width = False - new_col_pos = int(self.coords("rwl")[0]) self.hide_resize_and_ctrl_lines(ctrl_lines=False) - old_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] - size = new_col_pos - self.MT.col_positions[self.rsz_w - 1] - if size < self.MT.min_column_width: - new_col_pos = ceil(self.MT.col_positions[self.rsz_w - 1] + self.MT.min_column_width) - elif size > self.MT.max_column_width: - new_col_pos = floor(self.MT.col_positions[self.rsz_w - 1] + self.MT.max_column_width) - increment = new_col_pos - self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w + 1 :] = [ - e + increment for e in islice(self.MT.col_positions, self.rsz_w + 1, len(self.MT.col_positions)) - ] - self.MT.col_positions[self.rsz_w] = new_col_pos - new_width = self.MT.col_positions[self.rsz_w] - self.MT.col_positions[self.rsz_w - 1] - self.MT.allow_auto_resize_columns = False - self.MT.recreate_all_selection_boxes() - self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) - if self.column_width_resize_func is not None and old_width != new_width: - self.column_width_resize_func( - event_dict( - name="resize", - sheet=self.PAR.name, - resized_columns={self.rsz_w - 1: {"old_size": old_width, "new_size": new_width}}, - ) - ) elif self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: self.currently_resizing_height = False - self.hide_resize_and_ctrl_lines(ctrl_lines=False) - self.set_height(self.new_col_height, set_TL=True) - self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + self.drag_height_resize() elif ( self.drag_and_drop_enabled and self.col_selection_enabled @@ -1435,7 +1437,6 @@ def redraw_grid_and_text( x_stop, self.current_height, ) - draw_x = self.MT.col_positions[grid_start_col] yend = self.current_height - 5 if (self.PAR.ops.show_vertical_grid or self.width_resizing_enabled) and col_pos_exists: points = [ @@ -1760,8 +1761,7 @@ def open_text_editor( if self.text_editor.open and c == self.text_editor.column: self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) return - if self.text_editor.open: - self.hide_text_editor() + self.hide_text_editor() if not self.MT.see(r=0, c=c, keep_yscroll=True, check_cell_visibility=True): self.MT.refresh() x = self.MT.col_positions[c] + 1 @@ -1907,14 +1907,12 @@ def refresh_open_window_positions(self, zoom: Literal["in", "out"]) -> None: ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) - def hide_text_editor(self, reason: None | str = None) -> None: + def hide_text_editor(self) -> None: if self.text_editor.open: for binding in text_editor_to_unbind: self.text_editor.tktext.unbind(binding) self.itemconfig(self.text_editor.canvas_id, state="hidden") self.text_editor.open = False - if reason == "Escape": - self.focus_set() # c is displayed col def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: @@ -1933,6 +1931,7 @@ def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: return "break" if event.keysym == "Escape": self.hide_text_editor_and_dropdown() + self.focus_set() return # setting cell data with text editor value text_editor_value = self.text_editor.get() @@ -2011,7 +2010,7 @@ def dropdown_text_editor_modified( dd_window.search_and_see(event) def open_dropdown_window(self, c: int, event: object = None) -> None: - self.hide_text_editor("Escape") + self.hide_text_editor() kwargs = self.get_cell_kwargs(self.MT.datacn(c), key="dropdown") if kwargs["state"] == "normal": if not self.open_text_editor(event=event, c=c, dropdown=True): @@ -2075,8 +2074,9 @@ def open_dropdown_window(self, c: int, event: object = None) -> None: return redraw = False else: - self.dropdown.window.bind("", lambda _x: self.close_dropdown_window(c)) self.update_idletasks() + self.dropdown.window.bind("", lambda _x: self.close_dropdown_window(c)) + self.dropdown.window.bind("", self.close_dropdown_window) self.dropdown.window.focus_set() redraw = True self.dropdown.open = True @@ -2116,12 +2116,12 @@ def close_dropdown_window( edited = self.set_cell_data_undo(c, datacn=datacn, value=selection, redraw=not redraw) if edited: try_binding(self.extra_end_edit_cell_func, event_data) - self.focus_set() self.MT.recreate_all_selection_boxes() + self.focus_set() self.hide_text_editor_and_dropdown(redraw=redraw) def hide_text_editor_and_dropdown(self, redraw: bool = True) -> None: - self.hide_text_editor("Escape") + self.hide_text_editor() self.hide_dropdown_window() if redraw: self.MT.refresh() @@ -2174,7 +2174,16 @@ def set_cell_data_undo( ) edited = False if isinstance(self.MT._headers, int): - edited = self.MT.set_cell_data_undo(r=self.MT._headers, c=c, datacn=datacn, value=value, undo=True) + disprn = self.MT.try_disprn(self.MT._headers) + edited = self.MT.set_cell_data_undo( + r=disprn if isinstance(disprn, int) else 0, + c=c, + datarn=self.MT._headers, + datacn=datacn, + value=value, + undo=True, + cell_resize=isinstance(disprn, int), + ) else: self.fix_header(datacn) if not check_input_valid or self.input_valid_for_cell(datacn, value): @@ -2259,7 +2268,7 @@ def get_valid_cell_data_as_str(self, datacn: int, fix: bool = True) -> str: except Exception: value = "" if not value and self.PAR.ops.show_default_header_for_empty: - value = get_n2a(datacn, self.default_header) + value = get_n2a(datacn, self.PAR.ops.default_header) return value def get_value_for_empty_cell(self, datacn: int, c_ops: bool = True) -> object: diff --git a/thirdparty/tksheet/functions.py b/thirdparty/tksheet/functions.py index c2d1e4b..3f0f398 100644 --- a/thirdparty/tksheet/functions.py +++ b/thirdparty/tksheet/functions.py @@ -1,12 +1,14 @@ from __future__ import annotations -import bisect import csv import io import pickle import re import tkinter as tk import zlib +from bisect import ( + bisect_left, +) from collections import deque from collections.abc import ( Callable, @@ -35,8 +37,17 @@ def get_csv_str_dialect(s: str, delimiters: str) -> csv.Dialect: + if len(s) > 6000: + try: + _upto = next( + match.start() + 1 for i, match in enumerate(re.finditer("\n", s), 1) if i == 300 or match.start() > 6000 + ) + except Exception: + _upto = len(s) + else: + _upto = len(s) try: - return csv.Sniffer().sniff(s[:5000] if len(s) > 5000 else s, delimiters=delimiters) + return csv.Sniffer().sniff(s[:_upto] if len(s) > 6000 else s, delimiters=delimiters) except Exception: return csv.excel_tab @@ -208,6 +219,16 @@ def len_to_idx(n: int) -> int: return n - 1 +def b_index(sorted_seq: Sequence[int], num_to_index: int) -> int: + """ + Designed to be a faster way of finding the index of an int + in a sorted list of ints than list.index() + """ + if (idx := bisect_left(sorted_seq, num_to_index)) == len(sorted_seq) or sorted_seq[idx] != num_to_index: + raise ValueError(f"{num_to_index} is not in Sequence") + return idx + + def get_dropdown_kwargs( values: list = [], set_value: object = None, @@ -379,7 +400,7 @@ def get_seq_without_gaps_at_index( position: int, get_st_end: bool = False, ) -> tuple[int, int] | list[int]: - start_idx = bisect.bisect_left(seq, position) + start_idx = bisect_left(seq, position) forward_gap = get_index_of_gap_in_sorted_integer_seq_forward(seq, start_idx) reverse_gap = get_index_of_gap_in_sorted_integer_seq_reverse(seq, start_idx) if forward_gap is not None: @@ -418,6 +439,58 @@ def is_contiguous(iterable: Iterator[int]) -> bool: return all(i == (prev := prev + 1) for i in itr) +def down_cell_within_box( + r: int, + c: int, + r1: int, + c1: int, + r2: int, + c2: int, + numrows: int, + numcols: int, +) -> tuple[int, int]: + moved = False + new_r = r + new_c = c + if r + 1 == r2: + new_r = r1 + elif numrows > 1: + new_r = r + 1 + moved = True + if not moved: + if c + 1 == c2: + new_c = c1 + elif numcols > 1: + new_c = c + 1 + return new_r, new_c + + +def cell_right_within_box( + r: int, + c: int, + r1: int, + c1: int, + r2: int, + c2: int, + numrows: int, + numcols: int, +) -> tuple[int, int]: + moved = False + new_r = r + new_c = c + if c + 1 == c2: + new_c = c1 + elif numcols > 1: + new_c = c + 1 + moved = True + if not moved: + if r + 1 == r2: + new_r = r1 + elif numrows > 1: + new_r = r + 1 + return new_r, new_c + + def get_last( it: Iterator, ) -> object: diff --git a/thirdparty/tksheet/main_table.py b/thirdparty/tksheet/main_table.py index 93dcc40..f73be7b 100644 --- a/thirdparty/tksheet/main_table.py +++ b/thirdparty/tksheet/main_table.py @@ -49,10 +49,12 @@ try_to_bool, ) from .functions import ( + b_index, consecutive_ranges, decompress_load, diff_gen, diff_list, + down_cell_within_box, event_dict, gen_formatted, get_data_from_clipboard, @@ -71,6 +73,7 @@ new_tk_event, pickle_obj, pickled_event_dict, + cell_right_within_box, rounded_box_coords, span_idxs_post_move, try_binding, @@ -1778,7 +1781,12 @@ def get_cell_coords(self, r: int | None = None, c: int | None = None) -> tuple[f 0 if not r else self.row_positions[r + 1], ) - def cell_completely_visible(self, r: int = 0, c: int = 0, separate_axes: bool = False) -> bool: + def cell_completely_visible( + self, + r: int | None = 0, + c: int | None = 0, + separate_axes: bool = False, + ) -> bool: cx1, cy1, cx2, cy2 = self.get_canvas_visible_area() x1, y1, x2, y2 = self.get_cell_coords(r, c) x_vis = True @@ -1796,9 +1804,7 @@ def cell_completely_visible(self, r: int = 0, c: int = 0, separate_axes: bool = def cell_visible(self, r: int = 0, c: int = 0) -> bool: cx1, cy1, cx2, cy2 = self.get_canvas_visible_area() x1, y1, x2, y2 = self.get_cell_coords(r, c) - if x1 <= cx2 or y1 <= cy2 or x2 >= cx1 or y2 >= cy1: - return True - return False + return x1 <= cx2 or y1 <= cy2 or x2 >= cx1 or y2 >= cy1 def select_all(self, redraw: bool = True, run_binding_func: bool = True) -> None: selected = self.selected @@ -1834,8 +1840,10 @@ def select_cell( run_binding_func: bool = True, ext: bool = False, ) -> int: - self.deselect("all", redraw=False) + boxes_to_hide = tuple(self.selection_boxes) fill_iid = self.create_selection_box(r, c, r + 1, c + 1, state="hidden", ext=ext) + for iid in boxes_to_hide: + self.hide_selection_box(iid) if redraw: self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) if run_binding_func: @@ -2986,12 +2994,10 @@ def b1_motion(self, event: object): and self.selected.type_ == "cells" ): box = self.get_b1_motion_box( - *( - self.selected.row, - self.selected.column, - end_row, - end_col, - ) + self.selected.row, + self.selected.column, + end_row, + end_col, ) if ( box is not None @@ -3029,12 +3035,10 @@ def ctrl_b1_motion(self, event: object): and self.selected.type_ == "cells" ): box = self.get_b1_motion_box( - *( - self.selected.row, - self.selected.column, - end_row, - end_col, - ) + self.selected.row, + self.selected.column, + end_row, + end_col, ) if ( box is not None @@ -5457,7 +5461,7 @@ def main_table_redraw_grid_and_text( if self.PAR.ops.auto_resize_row_index and redraw_row_index and self.show_index: changed_w = self.RI.auto_set_index_width( end_row=grid_end_row, - only_rows=[self.datarn(r) for r in range(text_start_row, text_end_row)], + only_rows=map(self.datarn, range(text_start_row, text_end_row)), ) if resized_cols or resized_rows or changed_w: self.recreate_all_selection_boxes() @@ -6343,7 +6347,7 @@ def get_selected_rows( for r in range(box.coords.from_r, box.coords.upto_r) } if get_cells_as_rows: - return s | set(tup[0] for tup in self.get_selected_cells()) + return s | set(map(itemgetter(0), self.gen_selected_cells())) return s def get_selected_cols( @@ -6367,7 +6371,7 @@ def get_selected_cols( for c in range(box.coords.from_c, box.coords.upto_c) } if get_cells_as_cols: - return s | set(tup[1] for tup in self.get_selected_cells()) + return s | set(map(itemgetter(1), self.gen_selected_cells())) return s def get_selected_cells( @@ -6572,8 +6576,7 @@ def open_text_editor( if self.text_editor.open and (r, c) == self.text_editor.coords: self.text_editor.window.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) return - if self.text_editor.open: - self.hide_text_editor() + self.hide_text_editor() if not self.see(r=r, c=c, check_cell_visibility=True): self.refresh() x = self.col_positions[c] @@ -6707,14 +6710,12 @@ def refresh_open_window_positions(self, zoom: Literal["in", "out"]): ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) - def hide_text_editor(self, reason: None | str = None) -> None: + def hide_text_editor(self) -> None: if self.text_editor.open: for binding in text_editor_to_unbind: self.text_editor.tktext.unbind(binding) self.itemconfig(self.text_editor.canvas_id, state="hidden") self.text_editor.open = False - if reason == "Escape": - self.focus_set() def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: # checking if text editor should be closed or not @@ -6732,6 +6733,7 @@ def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: return "break" if event.keysym == "Escape": self.hide_text_editor_and_dropdown() + self.focus_set() return # setting cell data with text editor value text_editor_value = self.text_editor.get() @@ -6782,63 +6784,63 @@ def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: numrows = r2 - r1 if numcols == 1 and numrows == 1: if event.keysym == "Return": - self.select_cell(r + 1 if r < len(self.row_positions) - 2 else r, c) - self.see( - r + 1 if r < len(self.row_positions) - 2 else r, - c, - keep_xscroll=True, - bottom_right_corner=True, - check_cell_visibility=True, - ) + if self.PAR.ops.edit_cell_return == "right": + self.select_right(r, c) + if self.PAR.ops.edit_cell_return == "down": + self.select_down(r, c) + elif event.keysym == "Tab": + if self.PAR.ops.edit_cell_tab == "right": + self.select_right(r, c) + if self.PAR.ops.edit_cell_tab == "down": + self.select_down(r, c) + else: + if event.keysym == "Return": + if self.PAR.ops.edit_cell_return == "right": + new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols) + elif self.PAR.ops.edit_cell_return == "down": + new_r, new_c = down_cell_within_box(r, c, r1, c1, r2, c2, numrows, numcols) + else: + new_r, new_c = None, None elif event.keysym == "Tab": - self.select_cell(r, c + 1 if c < len(self.col_positions) - 2 else c) + if self.PAR.ops.edit_cell_tab == "right": + new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols) + elif self.PAR.ops.edit_cell_tab == "down": + new_r, new_c = down_cell_within_box(r, c, r1, c1, r2, c2, numrows, numcols) + else: + new_r, new_c = None, None + if isinstance(new_r, int): + self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid) self.see( - r, - c + 1 if c < len(self.col_positions) - 2 else c, - keep_xscroll=True, + new_r, + new_c, + keep_xscroll=False, bottom_right_corner=True, check_cell_visibility=True, ) - else: - moved = False - new_r = r - new_c = c - if event.keysym == "Return": - if r + 1 == r2: - new_r = r1 - elif numrows > 1: - new_r = r + 1 - moved = True - if not moved: - if c + 1 == c2: - new_c = c1 - elif numcols > 1: - new_c = c + 1 - elif event.keysym == "Tab": - if c + 1 == c2: - new_c = c1 - elif numcols > 1: - new_c = c + 1 - moved = True - if not moved: - if r + 1 == r2: - new_r = r1 - elif numrows > 1: - new_r = r + 1 - self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid) - self.see( - new_r, - new_c, - keep_xscroll=False, - bottom_right_corner=True, - check_cell_visibility=True, - ) self.recreate_all_selection_boxes() self.hide_text_editor_and_dropdown() if event.keysym != "FocusOut": self.focus_set() return "break" + def select_right(self, r: int, c: int) -> None: + self.select_cell(r, c + 1 if c < len(self.col_positions) - 2 else c) + self.see( + r, + c + 1 if c < len(self.col_positions) - 2 else c, + bottom_right_corner=True, + check_cell_visibility=True, + ) + + def select_down(self, r: int, c: int) -> None: + self.select_cell(r + 1 if r < len(self.row_positions) - 2 else r, c) + self.see( + r + 1 if r < len(self.row_positions) - 2 else r, + c, + bottom_right_corner=True, + check_cell_visibility=True, + ) + def tab_key(self, event: object = None) -> str: if not self.selected: return @@ -6851,19 +6853,7 @@ def tab_key(self, event: object = None) -> str: new_c = c + 1 if c < len(self.col_positions) - 2 else c self.select_cell(new_r, new_c) else: - moved = False - new_r = r - new_c = c - if c + 1 == c2: - new_c = c1 - elif numcols > 1: - new_c = c + 1 - moved = True - if not moved: - if r + 1 == r2: - new_r = r1 - elif numrows > 1: - new_r = r + 1 + new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols) self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid) self.see( new_r, @@ -6944,7 +6934,7 @@ def open_dropdown_window( c: int, event: object = None, ) -> None: - self.hide_text_editor("Escape") + self.hide_text_editor() datarn = self.datarn(r) datacn = self.datacn(c) kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown") @@ -7019,7 +7009,8 @@ def open_dropdown_window( else: self.update_idletasks() self.dropdown.window.bind("", lambda _: self.close_dropdown_window(r, c)) - self.dropdown.window.focus() + self.dropdown.window.bind("", self.close_dropdown_window) + self.dropdown.window.focus_set() redraw = True self.dropdown.open = True if redraw: @@ -7075,12 +7066,12 @@ def close_dropdown_window( ) if edited: try_binding(self.extra_end_edit_cell_func, event_data) - self.focus_set() self.recreate_all_selection_boxes() + self.focus_set() self.hide_text_editor_and_dropdown(redraw=redraw) def hide_text_editor_and_dropdown(self, redraw: bool = True) -> None: - self.hide_text_editor("Escape") + self.hide_text_editor() self.hide_dropdown_window() if redraw: self.refresh() @@ -7460,3 +7451,21 @@ def datacn(self, c: int) -> int: def datarn(self, r: int) -> int: return r if self.all_rows_displayed else self.displayed_rows[r] + + def dispcn(self, datacn: int) -> int: + return datacn if self.all_columns_displayed else b_index(self.displayed_columns, datacn) + + def try_dispcn(self, datacn: int) -> int | None: + try: + return self.dispcn(datacn) + except Exception: + return None + + def disprn(self, datarn: int) -> int: + return datarn if self.all_rows_displayed else b_index(self.displayed_rows, datarn) + + def try_disprn(self, datarn: int) -> int | None: + try: + return self.disprn(datarn) + except Exception: + return None diff --git a/thirdparty/tksheet/other_classes.py b/thirdparty/tksheet/other_classes.py index aef9a72..3a5e424 100644 --- a/thirdparty/tksheet/other_classes.py +++ b/thirdparty/tksheet/other_classes.py @@ -6,6 +6,7 @@ from functools import partial from typing import Literal + pickle_obj = partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL) FontTuple = namedtuple("FontTuple", "family size style") @@ -152,18 +153,16 @@ def format( redraw: bool = True, **kwargs, ) -> Span: - self["widget"].format( + return self["widget"].format( self, formatter_options={"formatter": formatter_class, **formatter_options, **kwargs}, formatter_class=formatter_class, redraw=redraw, **kwargs, ) - return self def del_format(self) -> Span: - self["widget"].del_format(self) - return self + return self["widget"].del_format(self) def highlight( self, @@ -173,7 +172,7 @@ def highlight( overwrite: bool = False, redraw: bool = True, ) -> Span: - self["widget"].highlight( + return self["widget"].highlight( self, bg=bg, fg=fg, @@ -181,34 +180,74 @@ def highlight( overwrite=overwrite, redraw=redraw, ) - return self def dehighlight(self, redraw: bool = True) -> Span: - self["widget"].dehighlight(self, redraw=redraw) + return self["widget"].dehighlight(self, redraw=redraw) del_highlight = dehighlight def readonly(self, readonly: bool = True) -> Span: - self["widget"].readonly(self, readonly=readonly) - return self + return self["widget"].readonly(self, readonly=readonly) - def dropdown(self, *args, **kwargs) -> Span: - self["widget"].dropdown(self, *args, **kwargs) + def dropdown( + self, + values: list = [], + edit_data: bool = True, + set_values: dict[tuple[int, int], object] = {}, + set_value: object = None, + state: str = "normal", + redraw: bool = True, + selection_function: Callable | None = None, + modified_function: Callable | None = None, + search_function: Callable | None = None, + validate_input: bool = True, + text: None | str = None, + ) -> Span: + return self["widget"].dropdown( + self, + values=values, + edit_data=edit_data, + set_values=set_values, + set_value=set_value, + state=state, + redraw=redraw, + selection_function=selection_function, + modified_function=modified_function, + search_function=search_function, + validate_input=validate_input, + text=text, + ) def del_dropdown(self) -> Span: - self["widget"].del_dropdown(self) + return self["widget"].del_dropdown(self) - def checkbox(self, *args, **kwargs) -> Span: - self["widget"].dropdown(self, *args, **kwargs) + def checkbox( + self, + edit_data: bool = True, + checked: bool | None = None, + state: str = "normal", + redraw: bool = True, + check_function: Callable | None = None, + text: str = "", + ) -> Span: + return self["widget"].checkbox( + self, + edit_data=edit_data, + checked=checked, + state=state, + redraw=redraw, + check_function=check_function, + text=text, + ) def del_checkbox(self) -> Span: - self["widget"].del_checkbox(self) + return self["widget"].del_checkbox(self) def align(self, align: str | None, redraw: bool = True) -> Span: - self["widget"].align(self, align=align, redraw=redraw) + return self["widget"].align(self, align=align, redraw=redraw) def del_align(self, redraw: bool = True) -> Span: - self["widget"].del_align(self, redraw=redraw) + return self["widget"].del_align(self, redraw=redraw) def clear(self, undo: bool | None = None, redraw: bool = True) -> Span: if undo is not None: @@ -304,7 +343,7 @@ def transpose(self) -> Span: self["transposed"] = not self["transposed"] return self - def expand(self, direction: str = "both") -> Span: + def expand(self, direction: Literal["both", "table", "down", "right"] = "both") -> Span: if direction == "both" or direction == "table": self["upto_r"], self["upto_c"] = None, None elif direction == "down": diff --git a/thirdparty/tksheet/row_index.py b/thirdparty/tksheet/row_index.py index f818610..641d5c3 100644 --- a/thirdparty/tksheet/row_index.py +++ b/thirdparty/tksheet/row_index.py @@ -140,7 +140,6 @@ def __init__(self, *args, **kwargs): self.hidd_boxes = set() self.align = kwargs["row_index_align"] - self.default_index = kwargs["default_row_index"].lower() self.tree_reset() self.basic_bindings() @@ -358,7 +357,16 @@ def mouse_motion(self, event: object) -> None: self.MT.current_cursor = "sb_v_double_arrow" else: self.rsz_h = None - if self.width_resizing_enabled and not mouse_over_resize: + if ( + self.width_resizing_enabled + and not mouse_over_resize + and self.PAR.ops.auto_resize_row_index is not True + and not ( + self.PAR.ops.auto_resize_row_index == "empty" + and not isinstance(self.MT._row_index, int) + and not self.MT._row_index + ) + ): try: x1, y1, x2, y2 = ( self.row_width_resize_bbox[0], @@ -480,7 +488,6 @@ def b1_press(self, event: object): if x < self.MT.min_column_width: x = int(self.MT.min_column_width) self.new_row_width = x - self.create_resize_line(x, y1, x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl") elif self.MT.identify_row(y=event.y, allow_end=False) is None: self.MT.deselect("all") elif self.row_selection_enabled and self.rsz_h is None and self.rsz_w is None: @@ -540,22 +547,21 @@ def b1_motion(self, event: object): fill=self.PAR.ops.resizing_line_fg, tag="rhl2", ) + self.drag_height_resize() elif self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: evx = event.x - self.hide_resize_and_ctrl_lines(ctrl_lines=False) if evx > self.current_width: x = self.MT.canvasx(evx - self.current_width) if evx > self.MT.max_index_width: evx = int(self.MT.max_index_width) x = self.MT.canvasx(evx - self.current_width) self.new_row_width = evx - self.MT.create_resize_line(x, y1, x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl") else: x = evx if x < self.MT.min_column_width: x = int(self.MT.min_column_width) self.new_row_width = x - self.create_resize_line(x, y1, x, y2, width=1, fill=self.PAR.ops.resizing_line_fg, tag="rwl") + self.drag_width_resize() if ( self.drag_and_drop_enabled and self.row_selection_enabled @@ -761,22 +767,48 @@ def fix_yview(self) -> None: self.MT.set_yviews("moveto", 1) def event_over_dropdown(self, r: int, datarn: int, event: object, canvasy: float) -> bool: - if ( + return ( canvasy < self.MT.row_positions[r] + self.MT.index_txt_height and self.get_cell_kwargs(datarn, key="dropdown") and event.x > self.current_width - self.MT.index_txt_height - 4 - ): - return True - return False + ) def event_over_checkbox(self, r: int, datarn: int, event: object, canvasy: float) -> bool: - if ( + return ( canvasy < self.MT.row_positions[r] + self.MT.index_txt_height and self.get_cell_kwargs(datarn, key="checkbox") and event.x < self.MT.index_txt_height + 4 - ): - return True - return False + ) + + def drag_width_resize(self) -> None: + self.set_width(self.new_row_width, set_TL=True) + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + + def drag_height_resize(self) -> None: + new_row_pos = int(self.coords("rhl")[1]) + old_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] + size = new_row_pos - self.MT.row_positions[self.rsz_h - 1] + if size < self.MT.min_row_height: + new_row_pos = ceil(self.MT.row_positions[self.rsz_h - 1] + self.MT.min_row_height) + elif size > self.MT.max_row_height: + new_row_pos = floor(self.MT.row_positions[self.rsz_h - 1] + self.MT.max_row_height) + increment = new_row_pos - self.MT.row_positions[self.rsz_h] + self.MT.row_positions[self.rsz_h + 1 :] = [ + e + increment for e in islice(self.MT.row_positions, self.rsz_h + 1, None) + ] + self.MT.row_positions[self.rsz_h] = new_row_pos + new_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] + self.MT.allow_auto_resize_rows = False + self.MT.recreate_all_selection_boxes() + self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + if self.row_height_resize_func is not None and old_height != new_height: + self.row_height_resize_func( + event_dict( + name="resize", + sheet=self.PAR.name, + resized_rows={self.rsz_h - 1: {"old_size": old_height, "new_size": new_height}}, + ) + ) def b1_release(self, event: object) -> None: if self.being_drawn_item is not None and (to_sel := self.MT.coords_and_type(self.being_drawn_item)): @@ -794,37 +826,12 @@ def b1_release(self, event: object) -> None: self.being_drawn_item = None self.MT.bind("", self.MT.mousewheel) if self.height_resizing_enabled and self.rsz_h is not None and self.currently_resizing_height: + self.drag_height_resize() self.currently_resizing_height = False - new_row_pos = int(self.coords("rhl")[1]) self.hide_resize_and_ctrl_lines(ctrl_lines=False) - old_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] - size = new_row_pos - self.MT.row_positions[self.rsz_h - 1] - if size < self.MT.min_row_height: - new_row_pos = ceil(self.MT.row_positions[self.rsz_h - 1] + self.MT.min_row_height) - elif size > self.MT.max_row_height: - new_row_pos = floor(self.MT.row_positions[self.rsz_h - 1] + self.MT.max_row_height) - increment = new_row_pos - self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h + 1 :] = [ - e + increment for e in islice(self.MT.row_positions, self.rsz_h + 1, len(self.MT.row_positions)) - ] - self.MT.row_positions[self.rsz_h] = new_row_pos - new_height = self.MT.row_positions[self.rsz_h] - self.MT.row_positions[self.rsz_h - 1] - self.MT.allow_auto_resize_rows = False - self.MT.recreate_all_selection_boxes() - self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) - if self.row_height_resize_func is not None and old_height != new_height: - self.row_height_resize_func( - event_dict( - name="resize", - sheet=self.PAR.name, - resized_rows={self.rsz_h - 1: {"old_size": old_height, "new_size": new_height}}, - ) - ) elif self.width_resizing_enabled and self.rsz_w is not None and self.currently_resizing_width: self.currently_resizing_width = False - self.hide_resize_and_ctrl_lines(ctrl_lines=False) - self.set_width(self.new_row_width, set_TL=True) - self.MT.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True) + self.drag_width_resize() if ( self.drag_and_drop_enabled and self.MT.anything_selected(exclude_cells=True, exclude_columns=True) @@ -1049,7 +1056,7 @@ def get_cell_dimensions(self, datarn: int) -> tuple[int, int]: align = self.align if align == "w": w += self.MT.index_txt_height - w += self.get_treeview_indent(self.MT._row_index[datarn].iid) + 5 + w += self.get_treeview_indent(self.MT._row_index[datarn].iid) + 10 return w, h def get_row_text_height( @@ -1129,9 +1136,6 @@ def get_index_text_width( isinstance(self.MT._row_index, int) and self.MT._row_index >= len(self.MT.data) ): return w - qconf = self.MT.txt_measure_canvas.itemconfig - qbbox = self.MT.txt_measure_canvas.bbox - qtxtm = self.MT.txt_measure_canvas_text if only_rows: iterable = only_rows elif self.MT.all_rows_displayed: @@ -1141,19 +1145,8 @@ def get_index_text_width( iterable = range(len(self.MT.data)) else: iterable = self.MT.displayed_rows - if ( - isinstance(self.MT._row_index, list) - and (tw := max(map(itemgetter(0), map(self.get_cell_dimensions, iterable)), default=w)) > w - ): - w = tw - elif isinstance(self.MT._row_index, int): - datacn = self.MT._row_index - for datarn in iterable: - if txt := self.MT.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True): - qconf(qtxtm, text=txt) - b = qbbox(qtxtm) - if (tw := b[2] - b[0] + 10) > w: - w = tw + if (new_w := max(map(itemgetter(0), map(self.get_cell_dimensions, iterable)), default=w)) > w: + w = new_w if w > self.MT.max_index_width: w = int(self.MT.max_index_width) return w @@ -1206,11 +1199,11 @@ def set_height_of_all_rows( def auto_set_index_width(self, end_row: int, only_rows: list) -> bool: if not isinstance(self.MT._row_index, int) and not self.MT._row_index: - if self.default_index == "letters": + if self.PAR.ops.default_row_index == "letters": new_w = self.MT.get_txt_w(f"{num2alpha(end_row)}") + 20 - elif self.default_index == "numbers": + elif self.PAR.ops.default_row_index == "numbers": new_w = self.MT.get_txt_w(f"{end_row}") + 20 - elif self.default_index == "both": + elif self.PAR.ops.default_row_index == "both": new_w = self.MT.get_txt_w(f"{end_row + 1} {num2alpha(end_row)}") + 20 elif self.PAR.ops.auto_resize_row_index is True: new_w = self.get_index_text_width(only_rows=only_rows) @@ -1342,33 +1335,44 @@ def redraw_tree_arrow( fill: str, tag: str | tuple[str], indent: float, + has_children: bool = False, open_: bool = False, ) -> None: mod = (self.MT.index_txt_height - 1) if self.MT.index_txt_height % 2 else self.MT.index_txt_height small_mod = int(mod / 5) mid_y = floor(self.MT.min_row_height / 2) - # up arrow - if open_: - points = ( - # the left hand downward point - x1 + 5 + indent, - y1 + mid_y + small_mod, - # the middle upward point - x1 + 5 + indent + small_mod + small_mod, - y1 + mid_y - small_mod, - # the right hand downward point - x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, - y1 + mid_y + small_mod, - ) - # right pointing arrow + if has_children: + # up arrow + if open_: + points = ( + # the left hand downward point + x1 + 5 + indent, + y1 + mid_y + small_mod, + # the middle upward point + x1 + 5 + indent + small_mod + small_mod, + y1 + mid_y - small_mod, + # the right hand downward point + x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, + y1 + mid_y + small_mod, + ) + # right pointing arrow + else: + points = ( + # the upper point + x1 + 5 + indent + small_mod + small_mod, + y1 + mid_y - small_mod - small_mod, + # the middle point + x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, + y1 + mid_y, + # the bottom point + x1 + 5 + indent + small_mod + small_mod, + y1 + mid_y + small_mod + small_mod, + ) else: points = ( # the upper point x1 + 5 + indent + small_mod + small_mod, y1 + mid_y - small_mod - small_mod, - # the middle point - x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, - y1 + mid_y, # the bottom point x1 + 5 + indent + small_mod + small_mod, y1 + mid_y + small_mod + small_mod, @@ -1377,14 +1381,14 @@ def redraw_tree_arrow( t, sh = self.hidd_tree_arrow.popitem() self.coords(t, points) if sh: - self.itemconfig(t, fill=fill) + self.itemconfig(t, fill=fill if has_children else self.PAR.ops.index_grid_fg) else: - self.itemconfig(t, fill=fill, tag=tag, state="normal") + self.itemconfig(t, fill=fill if has_children else self.PAR.ops.index_grid_fg, tag=tag, state="normal") self.lift(t) else: t = self.create_line( points, - fill=fill, + fill=fill if has_children else self.PAR.ops.index_grid_fg, tag=tag, width=2, capstyle=tk.ROUND, @@ -1531,7 +1535,6 @@ def redraw_grid_and_text( self.hidd_tree_arrow.update(self.disp_tree_arrow) self.disp_tree_arrow = {} self.visible_row_dividers = {} - draw_y = self.MT.row_positions[grid_start_row] xend = self.current_width - 6 self.row_width_resize_bbox = ( self.current_width - 2, @@ -1695,20 +1698,19 @@ def redraw_grid_and_text( draw_x += self.MT.index_txt_height + 3 indent = self.get_treeview_indent(iid) draw_x += indent + 5 - if self.tree[iid].children: - self.redraw_tree_arrow( - 2, - rtopgridln, - r=r, - fill=tree_arrow_fg, - tag="ta", - indent=indent, - open_=self.MT._row_index[datarn].iid in self.tree_open_ids, - ) + self.redraw_tree_arrow( + 2, + rtopgridln, + r=r, + fill=tree_arrow_fg, + tag="ta", + indent=indent, + has_children=bool(self.tree[iid].children), + open_=self.MT._row_index[datarn].iid in self.tree_open_ids, + ) lns = self.get_valid_cell_data_as_str(datarn, fix=False) if not lns: continue - lns = lns.split("\n") draw_y = rtopgridln + self.MT.index_first_ln_ins if mw > 5: draw_y = rtopgridln + self.MT.index_first_ln_ins @@ -1716,6 +1718,7 @@ def redraw_grid_and_text( if start_ln < 0: start_ln = 0 draw_y += start_ln * self.MT.index_xtra_lines_increment + lns = lns.split("\n") if draw_y + self.MT.index_half_txt_height - 1 <= rbotgridln and len(lns) > start_ln: for txt in islice(lns, start_ln, None): if self.hidd_text: @@ -1891,8 +1894,7 @@ def open_text_editor( if self.text_editor.open and r == self.text_editor.row: self.text_editor.set_text(self.text_editor.get() + "" if not isinstance(text, str) else text) return - if self.text_editor.open: - self.hide_text_editor() + self.hide_text_editor() if not self.MT.see(r=r, c=0, keep_yscroll=True, check_cell_visibility=True): self.MT.refresh() x = 0 @@ -2025,14 +2027,12 @@ def refresh_open_window_positions(self, zoom: Literal["in", "out"]) -> None: ) # self.itemconfig(self.dropdown.canvas_id, anchor=anchor, height=win_h) - def hide_text_editor(self, reason: None | str = None) -> None: + def hide_text_editor(self) -> None: if self.text_editor.open: for binding in text_editor_to_unbind: self.text_editor.tktext.unbind(binding) self.itemconfig(self.text_editor.canvas_id, state="hidden") self.text_editor.open = False - if reason == "Escape": - self.focus_set() # r is displayed row def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: @@ -2051,6 +2051,7 @@ def close_text_editor(self, event: tk.Event) -> Literal["break"] | None: return "break" if event.keysym == "Escape": self.hide_text_editor_and_dropdown() + self.focus_set() return text_editor_value = self.text_editor.get() r = self.text_editor.row @@ -2132,7 +2133,7 @@ def dropdown_text_editor_modified( # r is displayed row def open_dropdown_window(self, r: int, event: object = None) -> None: - self.hide_text_editor("Escape") + self.hide_text_editor() kwargs = self.get_cell_kwargs(self.MT.datarn(r), key="dropdown") if kwargs["state"] == "normal": if not self.open_text_editor(event=event, r=r, dropdown=True): @@ -2203,8 +2204,9 @@ def open_dropdown_window(self, r: int, event: object = None) -> None: return redraw = False else: - self.dropdown.window.bind("", lambda _x: self.close_dropdown_window(r)) self.update_idletasks() + self.dropdown.window.bind("", lambda _x: self.close_dropdown_window(r)) + self.dropdown.window.bind("", self.close_dropdown_window) self.dropdown.window.focus_set() redraw = True self.dropdown.open = True @@ -2245,12 +2247,12 @@ def close_dropdown_window( edited = self.set_cell_data_undo(r, datarn=datarn, value=selection, redraw=not redraw) if edited: try_binding(self.extra_end_edit_cell_func, event_data) - self.focus_set() self.MT.recreate_all_selection_boxes() + self.focus_set() self.hide_text_editor_and_dropdown(redraw=redraw) def hide_text_editor_and_dropdown(self, redraw: bool = True) -> None: - self.hide_text_editor("Escape") + self.hide_text_editor() self.hide_dropdown_window() if redraw: self.MT.refresh() @@ -2303,7 +2305,16 @@ def set_cell_data_undo( ) edited = False if isinstance(self.MT._row_index, int): - edited = self.MT.set_cell_data_undo(r=r, c=self.MT._row_index, datarn=datarn, value=value, undo=True) + dispcn = self.MT.try_dispcn(self.MT._row_index) + edited = self.MT.set_cell_data_undo( + r=r, + c=dispcn if isinstance(dispcn, int) else 0, + datarn=datarn, + datacn=self.MT._row_index, + value=value, + undo=True, + cell_resize=isinstance(dispcn, int), + ) else: self.fix_index(datarn) if not check_input_valid or self.input_valid_for_cell(datarn, value): @@ -2386,7 +2397,7 @@ def get_valid_cell_data_as_str(self, datarn: int, fix: bool = True) -> str: except Exception: value = "" if not value and self.PAR.ops.show_default_index_for_empty: - value = get_n2a(datarn, self.default_index) + value = get_n2a(datarn, self.PAR.ops.default_row_index) return value def get_value_for_empty_cell(self, datarn: int, r_ops: bool = True) -> object: @@ -2438,7 +2449,7 @@ def click_checkbox(self, r: int, datarn: int | None = None, undo: bool = True, r elif isinstance(self.MT._row_index, int): value = ( not self.MT.data[datarn][self.MT._row_index] - if type(self.MT.data[datarn][self.MT._row_index], bool) + if isinstance(self.MT.data[datarn][self.MT._row_index], bool) else False ) else: @@ -2483,8 +2494,7 @@ def ancestors_all_open(self, iid: str, stop_at: str | Node = "") -> bool: if iid not in self.tree_open_ids: return False return True - else: - return all(iid in self.tree_open_ids for iid in self.get_iid_ancestors(iid)) + return all(map(self.tree_open_ids.__contains__, self.get_iid_ancestors(iid))) def get_iid_ancestors(self, iid: str) -> Generator[str]: if self.tree[iid].parent: @@ -2520,7 +2530,7 @@ def remove_node_from_parents_children(self, node: Node) -> None: if not node.parent.children: self.tree_open_ids.discard(node.parent) - def pid_causes_recursive_loop(self, iid: str, pid: str) -> bool: + def build_pid_causes_recursive_loop(self, iid: str, pid: str) -> bool: return any( i == pid for i in chain( @@ -2528,3 +2538,8 @@ def pid_causes_recursive_loop(self, iid: str, pid: str) -> bool: islice(self.get_iid_ancestors(iid), 1, None), ) ) + + def move_pid_causes_recursive_loop(self, to_move_iid: str, move_to_parent: str) -> bool: + # if the parent the item is being moved under is one of the item's descendants + # then it is a recursive loop + return any(move_to_parent == diid for diid in self.get_iid_descendants(to_move_iid)) diff --git a/thirdparty/tksheet/sheet.py b/thirdparty/tksheet/sheet.py index 2814059..b25acc1 100644 --- a/thirdparty/tksheet/sheet.py +++ b/thirdparty/tksheet/sheet.py @@ -18,7 +18,7 @@ product, repeat, ) -from time import perf_counter +from timeit import default_timer from tkinter import ttk from typing import Literal @@ -188,6 +188,9 @@ def __init__( show_horizontal_grid: bool = True, display_selected_fg_over_highlights: bool = False, show_selected_cells_border: bool = True, + edit_cell_tab: Literal["right", "down", ""] = "right", + edit_cell_return: Literal["right", "down", ""] = "down", + editor_del_key: Literal["forward", "backward", ""] = "forward", treeview: bool = False, treeview_indent: str | int = "6", rounded_boxes: bool = True, @@ -339,11 +342,9 @@ def __init__( row_index_align=( convert_align(row_index_align) if row_index_align is not None else convert_align(index_align) ), - default_row_index=default_row_index, ) self.CH = ColumnHeaders( parent=self, - default_header=default_header, header_align=convert_align(header_align), ) self.MT = MainTable( @@ -376,7 +377,7 @@ def __init__( row_index_canvas=self.RI, header_canvas=self.CH, ) - self.unique_id = f"{perf_counter()}{self.winfo_id()}".replace(".", "") + self.unique_id = f"{default_timer()}{self.winfo_id()}".replace(".", "") style = ttk.Style() for orientation in ("Vertical", "Horizontal"): style.element_create( @@ -1502,6 +1503,8 @@ def reset( if displayed_rows: self.MT.displayed_rows = [] self.MT.all_rows_displayed = True + if selections: + self.MT.deselect(redraw=False) if row_heights: self.MT.saved_row_heights = {} self.MT.set_row_positions([]) @@ -1514,8 +1517,6 @@ def reset( self.MT.reset_tags() if undo_stack: self.reset_undos() - if selections: - self.MT.deselect(redraw=False) if sheet_options: self.ops = new_sheet_options() self.change_theme(redraw=False) @@ -1557,6 +1558,14 @@ def set_sheet_data( def data(self, value: list[list[object]]) -> None: self.data_reference(value) + def new_tksheet_event(self) -> EventDataDict: + return event_dict( + name="", + sheet=self.name, + widget=self, + selected=self.MT.selected, + ) + def set_data( self, *key: CreateSpanTypes, @@ -2185,6 +2194,7 @@ def del_rows( emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: + self.MT.deselect("all", redraw=False) rows = [rows] if isinstance(rows, int) else sorted(rows) event_data = event_dict( name="delete_rows", @@ -2213,7 +2223,6 @@ def del_rows( self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) - self.MT.deselect("all", redraw=False) self.set_refresh_timer(redraw) return event_data @@ -2227,6 +2236,7 @@ def del_columns( emit_event: bool = False, redraw: bool = True, ) -> EventDataDict: + self.MT.deselect("all", redraw=False) columns = [columns] if isinstance(columns, int) else sorted(columns) event_data = event_dict( name="delete_columns", @@ -2255,7 +2265,6 @@ def del_columns( self.MT.undo_stack.append(pickled_event_dict(event_data)) if emit_event: self.emit_event("<>", event_data) - self.MT.deselect("all", redraw=False) self.set_refresh_timer(redraw) return event_data @@ -2557,10 +2566,12 @@ def dropdown( redraw: bool = True, selection_function: Callable | None = None, modified_function: Callable | None = None, - search_function: Callable = dropdown_search_function, + search_function: Callable | None = None, validate_input: bool = True, text: None | str = None, ) -> Span: + if not search_function: + search_function = dropdown_search_function v = set_value if set_value is not None else values[0] if values else "" kwargs = { "values": values, @@ -2914,7 +2925,7 @@ def header_font(self, newfont: tuple[str, int, str] | None = None) -> tuple[str, def table_align( self, - align: str = None, + align: str | None = None, redraw: bool = True, ) -> str | Sheet: if align is None: @@ -2927,7 +2938,7 @@ def table_align( def header_align( self, - align: str = None, + align: str | None = None, redraw: bool = True, ) -> str | Sheet: if align is None: @@ -2940,7 +2951,7 @@ def header_align( def row_index_align( self, - align: str = None, + align: str | None = None, redraw: bool = True, ) -> str | Sheet: if align is None: @@ -3451,6 +3462,17 @@ def get_row_heights(self, canvas_positions: bool = False) -> list[float]: return self.MT.row_positions return self.MT.get_row_heights() + def get_safe_row_heights(self) -> list[int]: + default_h = self.MT.get_default_row_height() + return [0 if e == default_h else e for e in self.MT.gen_row_heights()] + + def set_safe_row_heights(self, heights: list[int]) -> Sheet: + default_h = self.MT.get_default_row_height() + self.MT.row_positions = list( + accumulate(chain([0], (self.valid_row_height(e) if e else default_h for e in heights))) + ) + return self + def get_row_text_height( self, row: int, @@ -3813,8 +3835,14 @@ def displayed_column_to_data(self, c: int) -> int: return c if self.MT.all_columns_displayed else self.MT.displayed_columns[c] data_c = displayed_column_to_data + datacn = displayed_column_to_data dcol = displayed_column_to_data + def data_column_to_displayed(self, c: int) -> int: + return self.MT.dispcn(c) + + dispcn = data_column_to_displayed + def display_columns( self, columns: None | Literal["all"] | Iterator[int] = None, @@ -3902,7 +3930,7 @@ def show_columns( idx = bisect_left(self.MT.displayed_columns, column) if len(self.MT.displayed_columns) == idx or self.MT.displayed_columns[idx] != column: self.MT.displayed_columns.insert(idx, column) - cws.insert(idx, self.MT.saved_column_widths.pop(column, self.PAR.ops.default_column_width)) + cws.insert(idx, self.MT.saved_column_widths.pop(column, self.ops.default_column_width)) self.MT.set_col_positions(cws) if deselect_all: self.MT.deselect(redraw=False) @@ -3941,8 +3969,14 @@ def displayed_row_to_data(self, r: int) -> int: return r if self.MT.all_rows_displayed else self.MT.displayed_rows[r] data_r = displayed_row_to_data + datarn = displayed_row_to_data drow = displayed_row_to_data + def data_row_to_displayed(self, r: int) -> int: + return self.MT.disprn(r) + + disprn = data_row_to_displayed + def display_rows( self, rows: None | Literal["all"] | Iterator[int] = None, @@ -4287,10 +4321,6 @@ def set_options(self, redraw: bool = True, **kwargs) -> Sheet: ) if "default_row_height" in kwargs: self.default_row_height(kwargs["default_row_height"]) - if "default_header" in kwargs: - self.CH.default_header = kwargs["default_header"].lower() - if "default_row_index" in kwargs: - self.RI.default_index = kwargs["default_row_index"].lower() if "max_column_width" in kwargs: self.MT.max_column_width = float(kwargs["max_column_width"]) if "max_row_height" in kwargs: @@ -4711,7 +4741,7 @@ def tree_build( self.RI.tree[iid].text = row[text_column] else: self.RI.tree[iid] = Node(row[text_column], iid, "") - if safety and (iid == pid or self.RI.pid_causes_recursive_loop(iid, pid)): + if safety and (iid == pid or self.RI.build_pid_causes_recursive_loop(iid, pid)): row[parent_column] = "" pid = "" if pid: @@ -5041,7 +5071,8 @@ def top_index_row(self, index: int) -> int: def move(self, item: str, parent: str, index: int | None = None) -> Sheet: """ Moves item to be under parent as child at index - 'parent' can be empty string which will make item a top node + 'parent' can be an empty str which will put the item at top level + Performance is not great """ if (item := item.lower()) and item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") @@ -5050,25 +5081,61 @@ def move(self, item: str, parent: str, index: int | None = None) -> Sheet: mapping = {} to_show = [] item_node = self.RI.tree[item] + item_r = self.RI.tree_rns[item] if parent: - if self.RI.pid_causes_recursive_loop(item, parent): + if self.RI.move_pid_causes_recursive_loop(item, parent): raise ValueError(f"iid '{item}' causes a recursive loop with parent '{parent}'.") parent_node = self.RI.tree[parent] if parent_node.children: if index is None or index >= len(parent_node.children): - index = len(parent_node.children) - 1 - item_r = self.RI.tree_rns[item] - new_r = self.RI.tree_rns[parent_node.children[index].iid] - new_r_desc = sum(1 for _ in self.RI.get_iid_descendants(parent_node.children[index].iid)) - item_desc = sum(1 for _ in self.RI.get_iid_descendants(item)) - if item_r < new_r: - r_ctr = new_r + new_r_desc - item_desc + index = len(parent_node.children) + new_r = self.RI.tree_rns[parent] + sum(1 for _ in self.RI.get_iid_descendants(parent)) + # new parent has children + # index is on end + # item row is less than move to row + if item_r < new_r: + r_ctr = new_r - sum(1 for _ in self.RI.get_iid_descendants(item)) + + # new parent has children + # index is on end + # item row is greater than move to row + else: + r_ctr = new_r + 1 else: - r_ctr = new_r + new_r = self.RI.tree_rns[parent_node.children[index].iid] + # new parent has children + # index is not end + # item row is less than move to row + if item_r < new_r: + if self.RI.items_parent(item) == parent: + r_ctr = ( + new_r + + sum(1 for _ in self.RI.get_iid_descendants(parent_node.children[index].iid)) + - sum(1 for _ in self.RI.get_iid_descendants(item)) + ) + else: + r_ctr = new_r - sum(1 for _ in self.RI.get_iid_descendants(item)) - 1 + + # new parent has children + # index is not end + # item row is greater than move to row + else: + r_ctr = new_r else: - if index is None: - index = 0 - r_ctr = self.RI.tree_rns[parent_node.iid] + 1 + index = 0 + new_r = self.RI.tree_rns[parent_node.iid] + + # new parent doesn't have children + # index always start + # item row is less than move to row + if item_r < new_r: + r_ctr = new_r - sum(1 for _ in self.RI.get_iid_descendants(item)) + + # new parent doesn't have children + # index always start + # item row is greater than move to row + else: + r_ctr = new_r + 1 mapping[item_r] = r_ctr if parent in self.RI.tree_open_ids and self.item_displayed(parent): to_show.append(r_ctr) @@ -5091,11 +5158,12 @@ def move(self, item: str, parent: str, index: int | None = None) -> Sheet: else: if (new_r := self.top_index_row(index)) is None: new_r = self.top_index_row((sum(1 for _ in self.RI.gen_top_nodes()) - 1)) - item_r = self.RI.tree_rns[item] if item_r < new_r: - par_desc = sum(1 for _ in self.RI.get_iid_descendants(self.rowitem(new_r, data_index=True))) - item_desc = sum(1 for _ in self.RI.get_iid_descendants(item)) - r_ctr = new_r + par_desc - item_desc + r_ctr = ( + new_r + + sum(1 for _ in self.RI.get_iid_descendants(self.rowitem(new_r, data_index=True))) + - sum(1 for _ in self.RI.get_iid_descendants(item)) + ) else: r_ctr = new_r mapping[item_r] = r_ctr @@ -6258,13 +6326,7 @@ def create_dropdown( ) -> Sheet: kwargs = get_dropdown_kwargs(*args, **kwargs) d = get_dropdown_dict(**kwargs) - if kwargs["set_value"] is None: - if kwargs["values"] and (v := self.MT.get_cell_data(r, c)) not in kwargs["values"]: - v = kwargs["values"][0] - else: - v == "" - else: - v = kwargs["set_value"] + v = kwargs["set_value"] if kwargs["set_value"] is not None else kwargs["values"][0] if kwargs["values"] else "" if isinstance(r, str) and r.lower() == "all" and isinstance(c, int): for r_ in range(self.MT.total_data_rows()): self._create_dropdown(r_, c, v, d) @@ -6633,9 +6695,6 @@ def get_index_dropdown_value(self, r: int = 0) -> object: if self.RI.get_cell_kwargs(r, key="dropdown"): return self.MT._row_index[r] - def delete_all_formatting(self, clear_values: bool = False) -> None: - self.MT.delete_all_formatting(clear_values=clear_values) - def format_cell( self, r: int | Literal["all"], diff --git a/thirdparty/tksheet/sheet_options.py b/thirdparty/tksheet/sheet_options.py index c9bb68b..3689163 100644 --- a/thirdparty/tksheet/sheet_options.py +++ b/thirdparty/tksheet/sheet_options.py @@ -1,5 +1,4 @@ from __future__ import annotations -from bgstally.utils import _ from .other_classes import ( DotDict, @@ -37,45 +36,45 @@ def new_sheet_options() -> DotDict: 13 if USER_OS == "darwin" else 11, "normal", ), - "edit_header_label": _("Edit header"), + "edit_header_label": "Edit header", "edit_header_accelerator": "", - "edit_index_label": _("Edit index"), + "edit_index_label": "Edit index", "edit_index_accelerator": "", - "edit_cell_label": _("Edit cell"), + "edit_cell_label": "Edit cell", "edit_cell_accelerator": "", - "cut_label": _("Cut"), + "cut_label": "Cut", "cut_accelerator": "Ctrl+X", - "cut_contents_label": _("Cut contents"), + "cut_contents_label": "Cut contents", "cut_contents_accelerator": "Ctrl+X", - "copy_label": _("Copy"), + "copy_label": "Copy", "copy_accelerator": "Ctrl+C", - "copy_contents_label": _("Copy contents"), + "copy_contents_label": "Copy contents", "copy_contents_accelerator": "Ctrl+C", - "paste_label": _("Paste"), + "paste_label": "Paste", "paste_accelerator": "Ctrl+V", - "delete_label": _("Delete"), + "delete_label": "Delete", "delete_accelerator": "Del", - "clear_contents_label": _("Clear contents"), + "clear_contents_label": "Clear contents", "clear_contents_accelerator": "Del", - "delete_columns_label": _("Delete columns"), + "delete_columns_label": "Delete columns", "delete_columns_accelerator": "", - "insert_columns_left_label": _("Insert columns left"), + "insert_columns_left_label": "Insert columns left", "insert_columns_left_accelerator": "", - "insert_column_label": _("Insert column"), + "insert_column_label": "Insert column", "insert_column_accelerator": "", - "insert_columns_right_label": _("Insert columns right"), + "insert_columns_right_label": "Insert columns right", "insert_columns_right_accelerator": "", - "delete_rows_label": _("Delete rows"), + "delete_rows_label": "Delete rows", "delete_rows_accelerator": "", - "insert_rows_above_label": _("Insert rows above"), + "insert_rows_above_label": "Insert rows above", "insert_rows_above_accelerator": "", - "insert_rows_below_label": _("Insert rows below"), + "insert_rows_below_label": "Insert rows below", "insert_rows_below_accelerator": "", - "insert_row_label": _("Insert row"), + "insert_row_label": "Insert row", "insert_row_accelerator": "", - "select_all_label": _("Select all"), + "select_all_label": "Select all", "select_all_accelerator": "Ctrl+A", - "undo_label": _("Undo"), + "undo_label": "Undo", "undo_accelerator": "Ctrl+Z", "copy_bindings": [ f"<{ctrl_key}-c>", @@ -99,7 +98,6 @@ def new_sheet_options() -> DotDict: ], "delete_bindings": [ "", - "", ], "select_all_bindings": [ f"<{ctrl_key}-a>", @@ -218,6 +216,8 @@ def new_sheet_options() -> DotDict: "default_row_height": "1", "default_column_width": 120, "default_row_index_width": 70, + "default_row_index": "numbers", + "default_header": "letters", "page_up_down_select_row": True, "paste_can_expand_x": False, "paste_can_expand_y": False, @@ -238,6 +238,9 @@ def new_sheet_options() -> DotDict: "show_horizontal_grid": True, "display_selected_fg_over_highlights": False, "show_selected_cells_border": True, + "edit_cell_tab": "right", + "edit_cell_return": "down", + "editor_del_key": "forward", "treeview": False, "treeview_indent": "6", "rounded_boxes": True, diff --git a/thirdparty/tksheet/text_editor.py b/thirdparty/tksheet/text_editor.py index 1eaed31..9f2220e 100644 --- a/thirdparty/tksheet/text_editor.py +++ b/thirdparty/tksheet/text_editor.py @@ -40,6 +40,7 @@ def __init__( self.bind(rc_binding, self.rc) self.bind(f"<{ctrl_key}-a>", self.select_all) self.bind(f"<{ctrl_key}-A>", self.select_all) + self.bind("", self.delete_key) self._orig = self._w + "_orig" self.tk.call("rename", self._w, self._orig) self.tk.createcommand(self._w, self._proxy) @@ -62,8 +63,9 @@ def reset( insertbackground=fg, state=state, ) + self.editor_del_key = sheet_ops.editor_del_key self.align = align - self.rc_popup_menu.delete(0, None) + self.rc_popup_menu.delete(0, "end") self.rc_popup_menu.add_command( label=sheet_ops.select_all_label, accelerator=sheet_ops.select_all_accelerator, @@ -126,6 +128,19 @@ def rc(self, event: object) -> None: self.focus_set() self.rc_popup_menu.tk_popup(event.x_root, event.y_root) + def delete_key(self, event: object = None) -> None: + if self.editor_del_key == "forward": + return + elif not self.editor_del_key: + return "break" + elif self.editor_del_key == "backward": + if self.tag_ranges("sel"): + return + if self.index("insert") == "1.0": + return "break" + self.delete("insert-1c") + return "break" + def select_all(self, event: object = None) -> Literal["break"]: self.tag_add(tk.SEL, "1.0", tk.END) self.mark_set(tk.INSERT, tk.END) diff --git a/thirdparty/tksheet/themes.py b/thirdparty/tksheet/themes.py index 2aa83a5..d1d927e 100644 --- a/thirdparty/tksheet/themes.py +++ b/thirdparty/tksheet/themes.py @@ -163,13 +163,13 @@ "header_border_fg": "#505054", "header_grid_fg": "#8C8C8C", "header_fg": "gray70", - "header_selected_cells_bg": "#545454", + "header_selected_cells_bg": "#4b4b4b", "header_selected_cells_fg": "#6aa2fc", "index_bg": "#141414", "index_border_fg": "#505054", "index_grid_fg": "#8C8C8C", "index_fg": "gray70", - "index_selected_cells_bg": "#545454", + "index_selected_cells_bg": "#4b4b4b", "index_selected_cells_fg": "#6aa2fc", "top_left_bg": "#28282a", "top_left_fg": "#505054", @@ -238,13 +238,13 @@ "header_border_fg": "#505054", "header_grid_fg": "#8C8C8C", "header_fg": "#FBB86C", - "header_selected_cells_bg": "#545454", + "header_selected_cells_bg": "#4b4b4b", "header_selected_cells_fg": "#FBB86C", "index_bg": "#000000", "index_border_fg": "#505054", "index_grid_fg": "#8C8C8C", "index_fg": "#FBB86C", - "index_selected_cells_bg": "#545454", + "index_selected_cells_bg": "#4b4b4b", "index_selected_cells_fg": "#FBB86C", "top_left_bg": "#141416", "top_left_fg": "#505054", diff --git a/thirdparty/tksheet/top_left_rectangle.py b/thirdparty/tksheet/top_left_rectangle.py index 5b16e4b..ae68d92 100644 --- a/thirdparty/tksheet/top_left_rectangle.py +++ b/thirdparty/tksheet/top_left_rectangle.py @@ -163,17 +163,16 @@ def set_dimensions( recreate_selection_boxes: bool = True, ) -> None: try: - self.update_idletasks() - if new_h is None: - h = self.winfo_height() - if new_w is None: - w = self.winfo_width() - if new_w: - self.config(width=new_w) - w = new_w - if new_h: - self.config(height=new_h) + if isinstance(new_h, int): h = new_h + self.config(height=h) + else: + h = self.CH.current_height + if isinstance(new_w, int): + w = new_w + self.config(width=w) + else: + w = self.RI.current_width except Exception: return self.coords(self.rw_box, 0, h - 5, w, h) From 28096990c11ff66f1b2f8de1870750052edc2949 Mon Sep 17 00:00:00 2001 From: aussig Date: Sat, 28 Sep 2024 10:31:54 +0100 Subject: [PATCH 03/27] Reapply removal of `timeit` module (not packaged in EDMC) from tksheet. --- server.py | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 server.py diff --git a/server.py b/server.py new file mode 100644 index 0000000..65c839b --- /dev/null +++ b/server.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# A web server to echo back a request's headers and data. +# +# Usage: ./webserver +# ./webserver 0.0.0.0:5000 + +from http.server import HTTPServer, BaseHTTPRequestHandler +from sys import argv +import json + +BIND_HOST = 'localhost' +PORT = 8008 + +discovery_response = json.dumps({ + "name": "This is a local test service", + "description": "This local test service does nothing, just logs out the requests.", + "url": "https://website.com/more-information", # URL to more information about the application, or the application home page + "version": "1.0.0", + "endpoints": { # If not present, defaults to all endpoints enabled. If present, only data for listed endpoints should be sent + "activities": + { + "min_period": 60 # Minimum number of seconds between requests. There will also be a hard minimum applied client-side (so values lower than that will be ignored). If omitted, use client default. + }, + "events": + { + "min_period": 15, # Minimum number of seconds between requests. There will also be a hard minimum applied client-side (so values lower than that will be ignored). If omitted, use client default. + "max_batch": 10 # Maximum number of events to include in a single request. Any remaining events will be sent in the next request. If omitted, use client default. + } + }, + "events": # If not present, accept default set of events. If present, only listed events should be sent to API (with optional further filtering). + { + "ApproachSettlement": + { + # Can be an empty object, in which case all occurrences of this event are sent + }, + "CollectCargo": + { + "filters": + { + "Type": "$UnknownArtifact2_name;" + } + }, + "Died": + { + "filters": + { + "KillerShip": "scout_hq|scout_nq|scout_q|scout|thargonswarm|thargon" + } + }, + "FactionKillBond": + { + "filters": + { + "AwardingFaction": "^\\$faction_PilotsFederation;$", + "VictimFaction": "^\\$faction_Thargoid;$" + } + }, + "FSDJump": {}, + "Location": {}, + "MissionAccepted": {}, + "MissionCompleted": {}, + "MissionFailed": {}, + "StartUp": {} + } +}).encode('utf-8') + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + if self.path.endswith("/discovery"): + self.write_response(discovery_response) + else: + self.write_response(b'') + + def do_POST(self): + content_length = int(self.headers.get('content-length', 0)) + body = self.rfile.read(content_length) + + self.write_response(body) + + def do_PUT(self): + content_length = int(self.headers.get('content-length', 0)) + body = self.rfile.read(content_length) + + self.write_response(body) + + def write_response(self, content): + self.send_response(200) + self.end_headers() + self.wfile.write(content) + + print(self.headers) + print(content.decode('utf-8')) + +if len(argv) > 1: + arg = argv[1].split(':') + BIND_HOST = arg[0] + PORT = int(arg[1]) + +print(f'Listening on http://{BIND_HOST}:{PORT}\n') + +httpd = HTTPServer((BIND_HOST, PORT), SimpleHTTPRequestHandler) +httpd.serve_forever() + + + From e1b42abbf3d0c11104c2145aced09a29977735b7 Mon Sep 17 00:00:00 2001 From: aussig Date: Sat, 28 Sep 2024 10:32:51 +0100 Subject: [PATCH 04/27] Revert "Reapply removal of `timeit` module (not packaged in EDMC) from tksheet." This reverts commit 28096990c11ff66f1b2f8de1870750052edc2949. --- server.py | 105 ------------------------------------------------------ 1 file changed, 105 deletions(-) delete mode 100644 server.py diff --git a/server.py b/server.py deleted file mode 100644 index 65c839b..0000000 --- a/server.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -# A web server to echo back a request's headers and data. -# -# Usage: ./webserver -# ./webserver 0.0.0.0:5000 - -from http.server import HTTPServer, BaseHTTPRequestHandler -from sys import argv -import json - -BIND_HOST = 'localhost' -PORT = 8008 - -discovery_response = json.dumps({ - "name": "This is a local test service", - "description": "This local test service does nothing, just logs out the requests.", - "url": "https://website.com/more-information", # URL to more information about the application, or the application home page - "version": "1.0.0", - "endpoints": { # If not present, defaults to all endpoints enabled. If present, only data for listed endpoints should be sent - "activities": - { - "min_period": 60 # Minimum number of seconds between requests. There will also be a hard minimum applied client-side (so values lower than that will be ignored). If omitted, use client default. - }, - "events": - { - "min_period": 15, # Minimum number of seconds between requests. There will also be a hard minimum applied client-side (so values lower than that will be ignored). If omitted, use client default. - "max_batch": 10 # Maximum number of events to include in a single request. Any remaining events will be sent in the next request. If omitted, use client default. - } - }, - "events": # If not present, accept default set of events. If present, only listed events should be sent to API (with optional further filtering). - { - "ApproachSettlement": - { - # Can be an empty object, in which case all occurrences of this event are sent - }, - "CollectCargo": - { - "filters": - { - "Type": "$UnknownArtifact2_name;" - } - }, - "Died": - { - "filters": - { - "KillerShip": "scout_hq|scout_nq|scout_q|scout|thargonswarm|thargon" - } - }, - "FactionKillBond": - { - "filters": - { - "AwardingFaction": "^\\$faction_PilotsFederation;$", - "VictimFaction": "^\\$faction_Thargoid;$" - } - }, - "FSDJump": {}, - "Location": {}, - "MissionAccepted": {}, - "MissionCompleted": {}, - "MissionFailed": {}, - "StartUp": {} - } -}).encode('utf-8') - -class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): - def do_GET(self): - if self.path.endswith("/discovery"): - self.write_response(discovery_response) - else: - self.write_response(b'') - - def do_POST(self): - content_length = int(self.headers.get('content-length', 0)) - body = self.rfile.read(content_length) - - self.write_response(body) - - def do_PUT(self): - content_length = int(self.headers.get('content-length', 0)) - body = self.rfile.read(content_length) - - self.write_response(body) - - def write_response(self, content): - self.send_response(200) - self.end_headers() - self.wfile.write(content) - - print(self.headers) - print(content.decode('utf-8')) - -if len(argv) > 1: - arg = argv[1].split(':') - BIND_HOST = arg[0] - PORT = int(arg[1]) - -print(f'Listening on http://{BIND_HOST}:{PORT}\n') - -httpd = HTTPServer((BIND_HOST, PORT), SimpleHTTPRequestHandler) -httpd.serve_forever() - - - From a72028ea4df7ac86756b5a9834093a234d024c96 Mon Sep 17 00:00:00 2001 From: aussig Date: Sat, 28 Sep 2024 10:33:23 +0100 Subject: [PATCH 05/27] Reapply removal of `timeit` module (not packaged in EDMC) from tksheet. --- thirdparty/tksheet/sheet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/thirdparty/tksheet/sheet.py b/thirdparty/tksheet/sheet.py index b25acc1..0abe234 100644 --- a/thirdparty/tksheet/sheet.py +++ b/thirdparty/tksheet/sheet.py @@ -18,7 +18,7 @@ product, repeat, ) -from timeit import default_timer +from time import perf_counter from tkinter import ttk from typing import Literal @@ -377,7 +377,7 @@ def __init__( row_index_canvas=self.RI, header_canvas=self.CH, ) - self.unique_id = f"{default_timer()}{self.winfo_id()}".replace(".", "") + self.unique_id = f"{perf_counter()}{self.winfo_id()}".replace(".", "") style = ttk.Style() for orientation in ("Vertical", "Horizontal"): style.element_create( From 6c01259e840ad08b5c401d427d4c3222ba6c8c48 Mon Sep 17 00:00:00 2001 From: aussig Date: Thu, 3 Oct 2024 18:46:31 +0100 Subject: [PATCH 06/27] Only check for a tick once per minute, not on every FSD jump. Closes #274. --- CHANGELOG.md | 2 ++ bgstally/bgstally.py | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fa912..38bdf38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## vx.x.x - xxxx-xx-xx +### Changes: +* Only check for a new tick once per minute, not on every FSD jump. ## v4.1.1 - 2024-09-27 diff --git a/bgstally/bgstally.py b/bgstally/bgstally.py index 8859472..ce28087 100644 --- a/bgstally/bgstally.py +++ b/bgstally/bgstally.py @@ -119,10 +119,6 @@ def journal_entry(self, cmdr, is_beta, system, station, entry, state): dirty: bool = False if entry.get('event') in ['StartUp', 'Location', 'FSDJump', 'CarrierJump']: - if self.check_tick(UpdateUIPolicy.IMMEDIATE): - # New activity will be generated with a new tick - activity = self.activity_manager.get_current_activity() - activity.system_entered(entry, self.state) dirty = True From 33a6e3a15b2fdf3f534f7db1f54bb6c01331f269 Mon Sep 17 00:00:00 2001 From: aussig Date: Mon, 7 Oct 2024 18:34:49 +0100 Subject: [PATCH 07/27] Reset problem displaying in overlay on successful display indicator --- bgstally/overlay.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bgstally/overlay.py b/bgstally/overlay.py index 6557d89..7b56447 100644 --- a/bgstally/overlay.py +++ b/bgstally/overlay.py @@ -121,6 +121,8 @@ def display_indicator(self, frame_name: str, ttl_override: int = None, fill_colo border_colour: str = border_colour_override if border_colour_override else fi['border_colour'] self.edmcoverlay.send_shape(f"bgstally-frame-{frame_name}", "rect", border_colour, fill_colour, int(fi['x']), int(fi['y']), int(fi['w']), int(fi['h']), ttl=ttl) + self.problem_displaying = False + except Exception as e: if not self.problem_displaying: # Only log a warning about failure once From c6262fba61d29af32f98ba0ac1b6bc8ed20251f9 Mon Sep 17 00:00:00 2001 From: aussig Date: Mon, 7 Oct 2024 18:37:29 +0100 Subject: [PATCH 08/27] Don't try displaying plugin message on update failure because cannot do this in a thread. --- CHANGELOG.md | 4 ++++ bgstally/updatemanager.py | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38bdf38..18b3e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * Only check for a new tick once per minute, not on every FSD jump. +### Bug Fixes: + +* If the check for a new plugin version was failing, this would throw several exceptions to the EDMC log. + ## v4.1.1 - 2024-09-27 diff --git a/bgstally/updatemanager.py b/bgstally/updatemanager.py index 2cee462..31fe7dc 100644 --- a/bgstally/updatemanager.py +++ b/bgstally/updatemanager.py @@ -38,7 +38,6 @@ def __init__(self, bgstally): # If you do accidentally overwrite your local folder, the plugin should have made a backup in "backups/" if path.exists(path.join(self.bgstally.plugin_dir, FILE_DISABLE)): Debug.logger.info(f"Disabling auto-update because {FILE_DISABLE} exists") - plug.show_error("{plugin_name}: Disabling auto-update because handbrake file exists".format(plugin_name=self.bgstally.plugin_name)) return try: @@ -59,7 +58,6 @@ def _version_info_received(self, success:bool, response:Response, request:BGSTal """ if not success: Debug.logger.warning("Unable to fetch latest plugin version") - plug.show_error(_("{plugin_name}: Unable to fetch latest plugin version").format(plugin_name=self.bgstally.plugin_name)) # LANG: Main window error message return version_data:dict = response.json() @@ -67,7 +65,6 @@ def _version_info_received(self, success:bool, response:Response, request:BGSTal if version_data['draft'] == True or version_data['prerelease'] == True: # This should never happen because the latest version URL excludes these, but in case GitHub has a wobble Debug.logger.info("Latest server version is draft or pre-release, ignoring") - plug.show_error(_("{plugin_name}: Unable to fetch latest plugin version").format(plugin_name=self.bgstally.plugin_name)) # LANG: Main window error message return # Check remote assets data structure @@ -93,7 +90,6 @@ def _download_received(self, success:bool, response:Response, request:BGSTallyRe """ if not success: Debug.logger.warning("Unable to fetch latest plugin download") - plug.show_error(_("{plugin_name}: Unable to fetch latest plugin download").format(plugin_name=self.bgstally.plugin_name)) # LANG: Main window error message return try: @@ -102,7 +98,6 @@ def _download_received(self, success:bool, response:Response, request:BGSTallyRe file.write(chunk) except Exception as e: Debug.logger.warning("Problem saving new version", exc_info=e) - plug.show_error(_("{plugin_name}: There was a problem saving the new version").format(plugin_name=self.bgstally.plugin_name)) # LANG: Main window error message return # Full success, download complete and available From 2fe9de49da39a2406069ad9ac8431ade4269746c Mon Sep 17 00:00:00 2001 From: aussig Date: Mon, 7 Oct 2024 18:39:05 +0100 Subject: [PATCH 09/27] Split "BM Prof" heading onto two lines. --- CHANGELOG.md | 1 + bgstally/windows/activity.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b3e1b..13374c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changes: * Only check for a new tick once per minute, not on every FSD jump. +* Split "BM Prof" heading on activity windows onto two lines for more efficient use of space. ### Bug Fixes: diff --git a/bgstally/windows/activity.py b/bgstally/windows/activity.py index 6229126..8ff666e 100644 --- a/bgstally/windows/activity.py +++ b/bgstally/windows/activity.py @@ -192,7 +192,8 @@ def show(self, activity: Activity): lbl_prof: ttk.Label = ttk.Label(frm_table, text=_("Prof"), font=FONT_HEADING_2) # LANG: Activity window column title, abbreviation for profit lbl_prof.grid(row=1, column=col, padx=2, pady=2); col += 1 ToolTip(lbl_prof, text=_("Profit at Z | L | M | H demand")) # LANG: Activity window tooltip for profit at zero | low | medium | high demand - lbl_bmprof: ttk.Label = ttk.Label(frm_table, text=_("BM Prof"), font=FONT_HEADING_2) # LANG: Activity window column title, abbreviation for black market profit + ttk.Label(frm_table, text="BM", font=FONT_HEADING_2, anchor=tk.CENTER).grid(row=0, column=col, padx=2) + lbl_bmprof: ttk.Label = ttk.Label(frm_table, text=_("Prof"), font=FONT_HEADING_2) # LANG: Activity window column title, abbreviation for black market profit lbl_bmprof.grid(row=1, column=col, padx=2, pady=2); col += 1 ToolTip(lbl_bmprof, text=_("Black market profit")) # LANG: Activity window tooltip lbl_bvs: ttk.Label = ttk.Label(frm_table, text="BVs", font=FONT_HEADING_2) # LANG: Activity window column title, abbreviation for bounty vouchers From 45d4f1a4ae626fa85919174f04fa94e56513a1de Mon Sep 17 00:00:00 2001 From: dwomble Date: Tue, 29 Oct 2024 04:19:39 -0700 Subject: [PATCH 10/27] CLB formatter (#278) * CLB formatter Code to format BGS reports the way we do for Celestial Light Brigade * Update CHANGELOG --- CHANGELOG.md | 4 + bgstally/formatters/clb.py | 290 +++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 bgstally/formatters/clb.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 13374c8..60318b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## vx.x.x - xxxx-xx-xx +### New Features: + +* Added new Discord formatter supporting the Celestial Light Brigade's preferred Discord structure and layout for BGS reports. + ### Changes: * Only check for a new tick once per minute, not on every FSD jump. diff --git a/bgstally/formatters/clb.py b/bgstally/formatters/clb.py new file mode 100644 index 0000000..f27f7f6 --- /dev/null +++ b/bgstally/formatters/clb.py @@ -0,0 +1,290 @@ +from datetime import datetime, timedelta, timezone +import time +import traceback +import re +from bgstally.activity import STATES_ELECTION, STATES_WAR, Activity +from bgstally.constants import CheckStates, DiscordActivity +from bgstally.debug import Debug +from bgstally.formatters.default import DefaultActivityFormatter +from bgstally.utils import _, __, human_format, is_number +from thirdparty.colors import * + +class CLBActivityFormatter(DefaultActivityFormatter): + """Activity formatter that outputs Lorum Ipsum + """ + + def __init__(self, bgstally): + """Instantiate class + + Args: + bgstally (BGSTally): The BGSTally object + """ + super().__init__(bgstally) + + + def get_name(self) -> str: + """Get the name of this formatter + + Returns: + str: The name of this formatter for choosing in the UI + """ + return 'Celestial Light Brigade' + + + def is_visible(self) -> bool: + """Should this formatter be visible to the user as a choice. + + Returns: + bool: True if visible, false if not + """ + return True + + def get_overlay(self, activity: Activity, activity_mode: DiscordActivity, system_names: list = None, lang: str = None) -> str: + """Get the in-game overlay text for a given instance of Activity. The in-game overlay + doesn't support any ANSI colouring and very few UTF-8 special characters. Basically, + only plain text is safe. + + Args: + activity (Activity): The Activity object containing the activity to post + activity_mode (DiscordActivity): Determines the type(s) of activity to post + system_names (list, optional): A list of system names to restrict the output for. If None, all systems are included. Defaults to None. + lang (str, optional): The language code for this post. Defaults to None. + + Returns: + str: The output text + """ + return self._build_text(activity, activity_mode, system_names, lang, False) + + def get_text(self, activity: Activity, activity_mode: DiscordActivity, system_names: list = None, lang: str = None) -> str: + """Generate formatted text for a given instance of Activity. Must be implemented by subclasses. + This method is used for getting the text for the 'copy and paste' function, and for direct posting + to Discord for those Formatters that use text style posts (vs Discord embed style posts) + + Args: + activity (Activity): The Activity object containing the activity to post + activity_mode (DiscordActivity): Determines the type(s) of activity to post + discord (bool, optional): True if the destination is Discord (so can include Discord-specific formatting such + as ```ansi blocks and UTF8 emoji characters), False if not. Defaults to False. + system_names (list, optional): A list of system names to restrict the output for. If None, all systems are included. Defaults to None. + lang (str, optional): The language code for this post. Defaults to None. + + Returns: + str: The output text + """ + return self._build_text(activity, activity_mode, system_names, lang, True) + + def get_fields(self, activity: Activity, activity_mode: DiscordActivity, system_names: list = None, lang: str = None) -> list: + """Generate a list of discord embed fields, conforming to the embed field spec defined here: + https://birdie0.github.io/discord-webhooks-guide/structure/embed/fields.html - i.e. each field should be a dict + containing 'name' and 'value' str keys, and optionally an 'inline' bool key + + Args: + activity (Activity): The Activity object containing the activity to post + activity_mode (DiscordActivity): Determines the type(s) of activity to post + system_names (list, optional): A list of system names to restrict the output for. If None, all systems are included. Defaults to None. + lang (str, optional): The language code for this post. Defaults to None. + + Returns: + list[dict]: A list of dicts, each containing an embed field containing 'name' and 'value' str keys, and optionally an 'inline' bool key + """ + + discord_fields = [] + for system in activity.systems.copy().values(): # Use a copy for thread-safe operation + if system_names is not None and system['System'] not in system_names: continue + system_text: str = "" + + if activity_mode == DiscordActivity.THARGOIDWAR or activity_mode == DiscordActivity.BOTH: + system_text += self._build_tw_system(system, True, lang) + + if (activity_mode == DiscordActivity.BGS or activity_mode == DiscordActivity.BOTH) and system.get('tw_status') is None: + for faction in system['Factions'].values(): + if faction['Enabled'] != CheckStates.STATE_ON: continue + system_text += self._build_faction(faction, True, lang) + + if system_text != "": + system_text = system_text.replace("'", "") + system_text = system_text.replace(" ", " ") + system_text = system_text.replace(" ", "") + discord_field = {'name': system['System'], 'value': f"```ansi\n{system_text}```"} + discord_fields.append(discord_field) + + return discord_fields + + # + # Private functions + # + + def _build_text(self, activity: Activity, activity_mode: DiscordActivity, system_names: list = None, lang: str = None, discord: bool = True) -> str: + try: + text:str = "" + # Force plain text if we are not posting to Discord + fp: bool = not discord + for system in activity.systems.copy().values(): # Use a copy for thread-safe operation + if system_names is not None and system['System'] not in system_names: + continue + + system_text:str = "" + + if activity_mode == DiscordActivity.THARGOIDWAR or activity_mode == DiscordActivity.BOTH: + system_text += self._build_tw_system(system, True, lang) + + if (activity_mode == DiscordActivity.BGS or activity_mode == DiscordActivity.BOTH) and system.get('tw_status') is None: + for faction in system['Factions'].values(): + if faction['Enabled'] != CheckStates.STATE_ON: + continue + system_text += self._build_faction(faction, DiscordActivity, lang) + + if system_text != "": + text += f" {color_wrap(system['System'], 'white', None, 'bold', fp=fp)}\n{system_text}" + + if discord and activity.discord_notes is not None and activity.discord_notes != "": + text += "\n" + activity.discord_notes + + offset = time.mktime(datetime.now().timetuple()) - time.mktime(datetime.now(timezone.utc).timetuple()) + tick = round(time.mktime(activity.tick_time.timetuple()) + offset) + text = f"### {__('BGS Report', lang)} - {__('Tick', lang)} : \n```ansi\n{text}```" + #else: + # text = "BGS Report - Tick : " + self.tick_time.strftime(DATETIME_FORMAT_TITLE) + "\n\n" + text + return text.replace("'", "") + + except BaseException as error: + return f"{traceback.format_exc()}\n An exception occurred: {error}" + + + def _build_inf_text(self, inf_data: dict, secondary_inf_data: dict, faction_state: str, discord: bool, lang:str) -> str: + """ + Create a complete summary of INF for the faction, including both primary and secondary if user has requested + + Args: + inf_data (dict): Dict containing INF, key = '1' - '5' or 'm' + secondary_inf_data (dict): Dict containing secondary INF, key = '1' - '5' or 'm' + faction_state (str): Current faction state + discord (bool): True if creating for Discord + + Returns: + str: INF summary + """ + fp: bool = not discord + + inf:int = sum((1 if k == 'm' else int(k)) * int(v) for k, v in inf_data.items()) + if self.bgstally.state.secondary_inf: + inf += sum((1 if k == 'm' else int(k)) * int(v) for k, v in secondary_inf_data.items()) + + if inf > 0: + return f"{green('+' + str(inf), fp=fp)} {blue(__('Inf', lang), fp=fp)}" + + return "" + + def _build_cz_text(self, cz_data: dict, prefix: str, discord: bool, lang: str) -> str: + """ + Create a summary of Conflict Zone activity + """ + if cz_data == {}: return "" + fp: bool = not discord + text:str = "" + + czs = [] + for w in ['h', 'm', 'l']: + if w in cz_data and int(cz_data[w]) != 0: + czs.append(f"{green(str(cz_data[w]), fp=fp)} {red(__(w.upper()+prefix, lang), fp=fp)}") + if len(czs) == 0: + return "" + + return ", ".join(czs) + + def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: + """ + Generate formatted text for a faction + """ + # Force plain text if we are not posting to Discord + fp: bool = not discord + + # Store the different item types in a list, we'll join them at the end to create a csv string + activity = [] + inf = self._build_inf_text(faction['MissionPoints'], faction['MissionPointsSecondary'], faction['FactionState'], discord, lang) + if inf != "": + activity.append(inf) + + for t, d in {'SpaceCZ': 'SCZ', 'GroundCZ': 'GCZ'}.items(): + cz = self._build_cz_text(faction.get(t, {}), d, discord) + if cz != "": + activity.append(cz) + + for action, desc in {'TradeBuy': 'Spend', 'TradeSell': "Profit"}.items(): + if sum(int(d['value']) for d in faction[action]) > 0: + if not self.bgstally.state.detailed_trade: + tot = 0 + for t, d in {0 : "[Z]", 1 : "[L]", 2 : "[M]", 3 : "[H]"}.items(): + if faction[action][t] and faction[action][t]['value'] > 0: + tot += faction[action][t]['value'] + activity.append(f"{human_format(tot)} {__(desc, lang)}") + else: + for t, d in {0 : "[Z]", 1 : "[L]", 2 : "[M]", 3 : "[H]"}.items(): + if faction[action][t] and faction[action][t]['value'] > 0: + activity.append(f"{human_format(faction[action][t]['value'])} {__(desc, lang)} {d} ({str(faction[action][t]['items'])}T)") + + # These are simple we can just loop through them + activities = {"Bounties" : "Bounties", + "CombatBonds" : "Bonds", + "BlackMarketProfit" : "BlackMarket", + "CartData" : "Carto", + "ExoData" : "Exo", + "Murdered" : "Ship Murders", + "GroundMurdered" : "Foot Murders", + "Scenarios" : "Scenarios", + "MissionFailed" : "Failed", + "SandR" : "S&R Units" + } + + for a in activities: + if faction.get(a): + amt = 0 + if isinstance(faction[a], int): + amt = faction[a] + if isinstance(faction[a], dict): + amt: int = sum(int(v) for k, v in faction[a].items()) + if amt > 0: + activity.append(f"{green(human_format(amt), fp=fp)} {__(activities[a], lang)}") + + activity_discord_text = ', '.join(activity) + + # Now do the detailed sections + if self.bgstally.state.detailed_inf: + if self.bgstally.state.secondary_inf: + for t, d in {'MissionPoints' : '[P]', 'MissionPointsSecondary' : '[S]'}.items(): + for i in range(1, 6): + if faction[t].get(str(i), 0) != 0: + activity_discord_text += grey(f"\n {faction[t][str(i)]} {d} {__('Inf', lang)}{'+' * i}", fp=fp) + else: + for i in range(1, 6): + if faction['MissionPoints'].get(str(i), 0) != 0: + activity_discord_text += grey(f"\n {faction['MissionPoints'][str(i)]} {__('Inf', lang)}{'+' * i}", fp=fp) + + # And the Search and Rescue details + if 'SandR' in faction: + for t, d in {'op': 'Occupied Escape Pod', 'dp' : 'Damaged Escape Pod', 'bb' : 'Black Box'}.items(): + if faction['SandR'][t]: + activity_discord_text += grey(f"\n {faction['SandR'][t]} {__(d, lang)}", fp=fp) + + if activity_discord_text == "": + return "" + + # Faction name and summary of activities + faction_name = self._process_faction_name(faction['Faction']) + faction_discord_text = f" {color_wrap(faction_name, 'yellow', None, 'bold', fp=fp)}: {activity_discord_text}\n" + + # Add ground settlement details further indented + for settlement_name in faction.get('GroundCZSettlements', {}): + if faction['GroundCZSettlements'][settlement_name]['enabled'] == CheckStates.STATE_ON: + faction_discord_text += grey(f" {faction['GroundCZSettlements'][settlement_name]['count']} {settlement_name}\n") + + return faction_discord_text + + def _process_faction_name(self, faction_name): + """ + Shorten the faction name if the user has chosen to + """ + if self.bgstally.state.abbreviate_faction_names: + return "".join((i if is_number(i) or "-" in i else i[0]) for i in faction_name.split()) + else: + return faction_name From 58d4a0e3a325c087f114bc7df5a415710622de67 Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 12:08:20 +0000 Subject: [PATCH 11/27] Remove URLs to Tez's original project (no longer exists), leaving credit in place. --- CHANGELOG.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60318b8..54f63f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -645,7 +645,7 @@ _* Note that the plugin only tracks primary and secondary INF from this version ## v1.0.0 - 2021-08-27 -Initial release, based on original [BGS-Tally-v2.0 project by tezw21](https://github.com/tezw21/BGS-Tally-v2.0) +Initial release, based on original BGS-Tally-v2.0 project by tezw21. ### New features: diff --git a/README.md b/README.md index 1420721..5adc06d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A tool to track and report your Background Simulation (BGS) and Thargoid War (TW) activity in Elite Dangerous, implemented as an [EDMC](https://github.com/EDCD/EDMarketConnector) plugin. BGS-Tally counts all the BGS / TW work you do for any faction, in any system. -Based on BGS-Tally v2.0 by tezw21: [Original tezw21 BGS-Tally-v2.0 Project](https://github.com/tezw21/BGS-Tally-v2.0) +Based on BGS-Tally v2.0 by tezw21 As well as all the BGS tracking from Tez's original version, this modified version includes: From 7b2594c3a5bfdc126a6b74541939efa63e42496e Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 12:08:43 +0000 Subject: [PATCH 12/27] Update reference to old tick detector. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5adc06d..fa3cf86 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ All of these files use the JSON format, so can be easily viewed in a text editor The plugin makes the following network connections: -1. To [EliteBGS](https://elitebgs.app/api/ebgs/v5/ticks) to grab the date and time of the lastest tick. +1. To [CMDR Zoy's Tick Detector](http://tick.infomancer.uk/galtick.json) to grab the date and time of the lastest tick. 2. To [GitHub](https://api.github.com/repos/aussig/BGS-Tally/releases/latest) to check the version of the plugin to see whether there is a newer version available. 3. To [Inara](https://inara.cz/elite/) to anonymously check for available information on targeted CMDRs. 4. **Only if configured by you** to a specific Discord webhook on a Discord server of your choice, and only when you explicitly click the _Post to Discord_ button. From 9019e5433269e4a00ed0101c3e7dbbfa8ac4df47 Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 12:09:39 +0000 Subject: [PATCH 13/27] Remove reference to old tick detector --- bgstally/activitymanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bgstally/activitymanager.py b/bgstally/activitymanager.py index 0eae429..5ceae65 100644 --- a/bgstally/activitymanager.py +++ b/bgstally/activitymanager.py @@ -67,7 +67,7 @@ def new_tick(self, tick: Tick, forced: bool) -> bool: if tick.tick_time < self.current_activity.tick_time: # An inbound tick is older than the current tick. The only valid situation for this is if the user has done a Force Tick - # but an elitebgs.app tick was then detected with an earlier timestamp. Ignore the tick in this situation. + # but a new tick was then detected with an earlier timestamp. Ignore the tick in this situation. return False else: # An inbound tick is newer than the current tick. Create a new Activity object. From 7a90105ef0437670297626457f996c957c78fad7 Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 17:30:38 +0000 Subject: [PATCH 14/27] Police murders now tracked Fixes #277 --- CHANGELOG.md | 1 + bgstally/activity.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f63f1..db2b48e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Bug Fixes: * If the check for a new plugin version was failing, this would throw several exceptions to the EDMC log. +* Murders of police ships were not being tracked. ## v4.1.1 - 2024-09-27 diff --git a/bgstally/activity.py b/bgstally/activity.py index 3da9ec9..16b3429 100644 --- a/bgstally/activity.py +++ b/bgstally/activity.py @@ -704,7 +704,7 @@ def trade_sold(self, journal_entry:dict, state:State): self.recalculate_zero_activity() - def ship_targeted(self, journal_entry: Dict, state: State): + def ship_targeted(self, journal_entry: dict, state: State): """ Handle targeting a ship """ @@ -719,7 +719,11 @@ def ship_targeted(self, journal_entry: Dict, state: State): state.last_ship_targeted = {'Faction': journal_entry['Faction'], 'PilotName': journal_entry['PilotName'], 'PilotName_Localised': journal_entry['PilotName_Localised']} - state.last_ships_targeted[journal_entry['PilotName_Localised']] = state.last_ship_targeted + + if journal_entry['PilotName'].startswith("$ShipName_Police"): + state.last_ships_targeted[journal_entry['PilotName']] = state.last_ship_targeted + else: + state.last_ships_targeted[journal_entry['PilotName_Localised']] = state.last_ship_targeted if 'Faction' in journal_entry and state.last_spacecz_approached != {} and state.last_spacecz_approached.get('ally_faction') is not None: # In space CZ, check we're targeting the right faction @@ -727,11 +731,11 @@ def ship_targeted(self, journal_entry: Dict, state: State): self.bgstally.ui.show_warning(_("Targeted Ally!")) # LANG: Overlay message - def crime_committed(self, journal_entry: Dict, state: State): + def crime_committed(self, journal_entry: dict, state: State): """ Handle a crime """ - current_system = self.systems.get(state.current_system_id) + current_system: dict|None = self.systems.get(state.current_system_id) if not current_system: return self.dirty = True @@ -744,7 +748,7 @@ def crime_committed(self, journal_entry: Dict, state: State): match journal_entry['CrimeType']: case 'murder': # For ship murders, if we didn't get a previous scan containing ship faction, don't log - ship_target_info:dict = state.last_ships_targeted.pop(journal_entry.get('Victim'), None) + ship_target_info: dict = state.last_ships_targeted.pop(journal_entry.get('Victim'), None) if ship_target_info is None: return faction = current_system['Factions'].get(ship_target_info.get('Faction')) From e7ea85f4a80195fca1c4c8d6a9ab4ed1b2f5e616 Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 17:39:15 +0000 Subject: [PATCH 15/27] Fix exception in CLB formatter. --- bgstally/formatters/clb.py | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/bgstally/formatters/clb.py b/bgstally/formatters/clb.py index f27f7f6..e05cfe3 100644 --- a/bgstally/formatters/clb.py +++ b/bgstally/formatters/clb.py @@ -32,7 +32,7 @@ def get_name(self) -> str: def is_visible(self) -> bool: - """Should this formatter be visible to the user as a choice. + """Should this formatter be visible to the user as a choice. Returns: bool: True if visible, false if not @@ -72,7 +72,7 @@ def get_text(self, activity: Activity, activity_mode: DiscordActivity, system_na str: The output text """ return self._build_text(activity, activity_mode, system_names, lang, True) - + def get_fields(self, activity: Activity, activity_mode: DiscordActivity, system_names: list = None, lang: str = None) -> list: """Generate a list of discord embed fields, conforming to the embed field spec defined here: https://birdie0.github.io/discord-webhooks-guide/structure/embed/fields.html - i.e. each field should be a dict @@ -120,9 +120,9 @@ def _build_text(self, activity: Activity, activity_mode: DiscordActivity, system # Force plain text if we are not posting to Discord fp: bool = not discord for system in activity.systems.copy().values(): # Use a copy for thread-safe operation - if system_names is not None and system['System'] not in system_names: + if system_names is not None and system['System'] not in system_names: continue - + system_text:str = "" if activity_mode == DiscordActivity.THARGOIDWAR or activity_mode == DiscordActivity.BOTH: @@ -130,14 +130,14 @@ def _build_text(self, activity: Activity, activity_mode: DiscordActivity, system if (activity_mode == DiscordActivity.BGS or activity_mode == DiscordActivity.BOTH) and system.get('tw_status') is None: for faction in system['Factions'].values(): - if faction['Enabled'] != CheckStates.STATE_ON: + if faction['Enabled'] != CheckStates.STATE_ON: continue system_text += self._build_faction(faction, DiscordActivity, lang) if system_text != "": text += f" {color_wrap(system['System'], 'white', None, 'bold', fp=fp)}\n{system_text}" - - if discord and activity.discord_notes is not None and activity.discord_notes != "": + + if discord and activity.discord_notes is not None and activity.discord_notes != "": text += "\n" + activity.discord_notes offset = time.mktime(datetime.now().timetuple()) - time.mktime(datetime.now(timezone.utc).timetuple()) @@ -146,10 +146,10 @@ def _build_text(self, activity: Activity, activity_mode: DiscordActivity, system #else: # text = "BGS Report - Tick : " + self.tick_time.strftime(DATETIME_FORMAT_TITLE) + "\n\n" + text return text.replace("'", "") - + except BaseException as error: return f"{traceback.format_exc()}\n An exception occurred: {error}" - + def _build_inf_text(self, inf_data: dict, secondary_inf_data: dict, faction_state: str, discord: bool, lang:str) -> str: """ @@ -189,13 +189,13 @@ def _build_cz_text(self, cz_data: dict, prefix: str, discord: bool, lang: str) - czs.append(f"{green(str(cz_data[w]), fp=fp)} {red(__(w.upper()+prefix, lang), fp=fp)}") if len(czs) == 0: return "" - + return ", ".join(czs) def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: """ Generate formatted text for a faction - """ + """ # Force plain text if we are not posting to Discord fp: bool = not discord @@ -204,9 +204,9 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: inf = self._build_inf_text(faction['MissionPoints'], faction['MissionPointsSecondary'], faction['FactionState'], discord, lang) if inf != "": activity.append(inf) - + for t, d in {'SpaceCZ': 'SCZ', 'GroundCZ': 'GCZ'}.items(): - cz = self._build_cz_text(faction.get(t, {}), d, discord) + cz = self._build_cz_text(faction.get(t, {}), d, discord, lang) if cz != "": activity.append(cz) @@ -217,7 +217,7 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: for t, d in {0 : "[Z]", 1 : "[L]", 2 : "[M]", 3 : "[H]"}.items(): if faction[action][t] and faction[action][t]['value'] > 0: tot += faction[action][t]['value'] - activity.append(f"{human_format(tot)} {__(desc, lang)}") + activity.append(f"{human_format(tot)} {__(desc, lang)}") else: for t, d in {0 : "[Z]", 1 : "[L]", 2 : "[M]", 3 : "[H]"}.items(): if faction[action][t] and faction[action][t]['value'] > 0: @@ -235,7 +235,7 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: "MissionFailed" : "Failed", "SandR" : "S&R Units" } - + for a in activities: if faction.get(a): amt = 0 @@ -259,7 +259,7 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: for i in range(1, 6): if faction['MissionPoints'].get(str(i), 0) != 0: activity_discord_text += grey(f"\n {faction['MissionPoints'][str(i)]} {__('Inf', lang)}{'+' * i}", fp=fp) - + # And the Search and Rescue details if 'SandR' in faction: for t, d in {'op': 'Occupied Escape Pod', 'dp' : 'Damaged Escape Pod', 'bb' : 'Black Box'}.items(): @@ -268,18 +268,18 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: if activity_discord_text == "": return "" - + # Faction name and summary of activities faction_name = self._process_faction_name(faction['Faction']) faction_discord_text = f" {color_wrap(faction_name, 'yellow', None, 'bold', fp=fp)}: {activity_discord_text}\n" - + # Add ground settlement details further indented for settlement_name in faction.get('GroundCZSettlements', {}): if faction['GroundCZSettlements'][settlement_name]['enabled'] == CheckStates.STATE_ON: faction_discord_text += grey(f" {faction['GroundCZSettlements'][settlement_name]['count']} {settlement_name}\n") return faction_discord_text - + def _process_faction_name(self, faction_name): """ Shorten the faction name if the user has chosen to @@ -287,4 +287,4 @@ def _process_faction_name(self, faction_name): if self.bgstally.state.abbreviate_faction_names: return "".join((i if is_number(i) or "-" in i else i[0]) for i in faction_name.split()) else: - return faction_name + return faction_name From 5c9e7906b4bcb544f1c1a65437e426f5a8002ade Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 18:21:28 +0000 Subject: [PATCH 16/27] API key now cleaned and truncated Fixes #279 --- CHANGELOG.md | 1 + bgstally/api.py | 24 ++++++++++++------------ bgstally/utils.py | 14 ++++++++++++++ bgstally/windows/api.py | 4 ++-- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db2b48e..3f023bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * If the check for a new plugin version was failing, this would throw several exceptions to the EDMC log. * Murders of police ships were not being tracked. +* Any API key entered by the user is now cleaned up and truncated to avoid problems with bad inputs. ## v4.1.1 - 2024-09-27 diff --git a/bgstally/api.py b/bgstally/api.py index 36f3615..1c32bb0 100644 --- a/bgstally/api.py +++ b/bgstally/api.py @@ -11,7 +11,7 @@ from bgstally.constants import RequestMethod from bgstally.debug import Debug from bgstally.requestmanager import BGSTallyRequest -from bgstally.utils import get_by_path +from bgstally.utils import get_by_path, string_to_alphanumeric API_VERSION = "1.5.0" @@ -97,23 +97,23 @@ def as_dict(self) -> dict: } - def from_dict(self, data:dict): + def from_dict(self, data: dict): """ Populate our user and discovery state from a dict """ # User state - self.url:str = data['url'] - self.key:str = data['key'] - self.activities_enabled:bool = data['activities_enabled'] - self.events_enabled:bool = data['events_enabled'] - self.user_approved:bool = data['user_approved'] + self.url: str = data['url'] + self.key: str = string_to_alphanumeric(data['key'])[:128] + self.activities_enabled: bool = data['activities_enabled'] + self.events_enabled: bool = data['events_enabled'] + self.user_approved: bool = data['user_approved'] # Discovery state - self.name:str = data['name'] - self.version:semantic_version = semantic_version.Version.coerce(data['version']) - self.description:str = data['description'] - self.endpoints:dict = data['endpoints'] - self.events:dict = data['events'] + self.name: str = data['name'] + self.version: semantic_version = semantic_version.Version.coerce(data['version']) + self.description: str = data['description'] + self.endpoints: dict = data['endpoints'] + self.events: dict = data['events'] def discover(self, callback:callable): diff --git a/bgstally/utils.py b/bgstally/utils.py index c5f364e..16d023c 100644 --- a/bgstally/utils.py +++ b/bgstally/utils.py @@ -1,4 +1,5 @@ import functools +import re from os import listdir from os.path import join from pathlib import Path @@ -141,3 +142,16 @@ def all_subclasses(cls: type) -> set[type]: set[type]: A set of Python subclasses """ return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)]) + + +def string_to_alphanumeric(s: str) -> str: + """Clean a string so it only contains alphanumeric characters + + Args: + s (str): The string to clean + + Returns: + str: The cleaned string + """ + pattern: re.Pattern = re.compile('[\W_]+') + return pattern.sub('', s) diff --git a/bgstally/windows/api.py b/bgstally/windows/api.py index 3857668..9e7e051 100644 --- a/bgstally/windows/api.py +++ b/bgstally/windows/api.py @@ -8,7 +8,7 @@ from bgstally.api import API from bgstally.constants import FOLDER_ASSETS, FONT_HEADING_2 from bgstally.debug import Debug -from bgstally.utils import _ +from bgstally.utils import _, string_to_alphanumeric from bgstally.widgets import CollapsibleFrame, EntryPlus, HyperlinkManager from requests import Response from bgstally.requestmanager import BGSTallyRequest @@ -162,7 +162,7 @@ def _field_edited(self, widget:tk.Widget, *args): self.discovery_done = False self.api.url = self.entry_apiurl.get().rstrip('/') + '/' - self.api.key = self.entry_apikey.get() + self.api.key = string_to_alphanumeric(self.entry_apikey.get())[:128] self.api.activities_enabled = self.cb_apiactivities.instate(['selected']) self.api.events_enabled = self.cb_apievents.instate(['selected']) self._update() From ed05e86dd90e7895284b0135c728e3388688f08f Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 19:24:22 +0000 Subject: [PATCH 17/27] Store and show faction Influence % for each faction Fixes #276 --- CHANGELOG.md | 1 + bgstally/activity.py | 26 +++++++++++++++----------- bgstally/windows/activity.py | 2 ++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f023bb..fa405e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### New Features: * Added new Discord formatter supporting the Celestial Light Brigade's preferred Discord structure and layout for BGS reports. +* Each faction now has its influence % shown in the on-screen activity window. ### Changes: diff --git a/bgstally/activity.py b/bgstally/activity.py index 16b3429..5ec0404 100644 --- a/bgstally/activity.py +++ b/bgstally/activity.py @@ -271,7 +271,7 @@ def clear_activity(self, mission_log: MissionLog): sum(int(d['scooped']) for d in system['TWSandR'].values()) > 0: # The system has a current mission, or it's the current system, or it has TWSandR scoops - zero, don't delete for faction_name, faction_data in system['Factions'].items(): - system['Factions'][faction_name] = self._get_new_faction_data(faction_name, faction_data['FactionState']) + system['Factions'][faction_name] = self._get_new_faction_data(faction_name, faction_data['FactionState'], faction_data['Influence']) system['TWKills'] = self._get_new_tw_kills_data() # Note: system['TWSandR'] scooped data is carried forward, delivered data is cleared for d in system['TWSandR'].values(): @@ -313,15 +313,16 @@ def system_entered(self, journal_entry: Dict, state: State): if faction['Name'] == "Pilots' Federation Local Branch": continue # Ignore conflict states in FactionState as we can't trust they always come in pairs. We deal with conflicts separately below. - faction_state = faction['FactionState'] if faction['FactionState'] not in STATES_WAR and faction['FactionState'] not in STATES_ELECTION else "None" + faction_state: str = faction['FactionState'] if faction['FactionState'] not in STATES_WAR and faction['FactionState'] not in STATES_ELECTION else "None" + faction_inf: float = faction['Influence'] if faction['Name'] in current_system['Factions']: # We have this faction, ensure it's up to date with latest state faction_data = current_system['Factions'][faction['Name']] - self._update_faction_data(faction_data, faction_state) + self._update_faction_data(faction_data, faction_state, faction_inf) else: # We do not have this faction, create a new clean entry - current_system['Factions'][faction['Name']] = self._get_new_faction_data(faction['Name'], faction_state) + current_system['Factions'][faction['Name']] = self._get_new_faction_data(faction['Name'], faction_state, faction_inf) # Set war states for pairs of factions in War / Civil War / Elections for conflict in journal_entry.get('Conflicts', []): @@ -1126,9 +1127,9 @@ def get_sample_system_data(self) -> dict: return {'System': "Sample System Name", 'SystemAddress': 1, 'zero_system_activity': False, - 'Factions': {"Sample Faction Name 1": self._get_new_faction_data("Sample Faction Name 1", "None", True), - "Sample Faction Name 2": self._get_new_faction_data("Sample Faction Name 2", "None", True), - "Sample Faction Name 3": self._get_new_faction_data("Sample Faction Name 3", "None", True)}, + 'Factions': {"Sample Faction Name 1": self._get_new_faction_data("Sample Faction Name 1", "None", 40, True), + "Sample Faction Name 2": self._get_new_faction_data("Sample Faction Name 2", "None", 30, True), + "Sample Faction Name 3": self._get_new_faction_data("Sample Faction Name 3", "None", 30, True)}, 'TWKills': self._get_new_tw_kills_data(True), 'TWSandR': self._get_new_tw_sandr_data(True), 'TWReactivate': 5} @@ -1154,7 +1155,7 @@ def _get_new_system_data(self, system_name: str, system_address: str, faction_da 'TWReactivate': 0} - def _get_new_faction_data(self, faction_name: str, faction_state: str, sample: bool = False) -> dict: + def _get_new_faction_data(self, faction_name: str, faction_state: str, faction_inf: float, sample: bool = False) -> dict: """Get a new data structure for storing faction data Args: @@ -1166,7 +1167,7 @@ def _get_new_faction_data(self, faction_name: str, faction_state: str, sample: b dict: The faction data """ s: bool = sample # Shorter - return {'Faction': faction_name, 'FactionState': faction_state, 'Enabled': self.bgstally.state.EnableSystemActivityByDefault.get(), + return {'Faction': faction_name, 'FactionState': faction_state, 'Influence': faction_inf, 'Enabled': self.bgstally.state.EnableSystemActivityByDefault.get(), 'MissionPoints': {'1': 3 if s else 0, '2': 4 if s else 0, '3': 5 if s else 0, '4': 6 if s else 0, '5': 7 if s else 0, 'm': 8 if s else 0}, 'MissionPointsSecondary': {'1': 3 if s else 0, '2': 4 if s else 0, '3': 5 if s else 0, '4': 6 if s else 0, '5': 7 if s else 0, 'm': 8 if s else 0}, 'BlackMarketProfit': 50000 if s else 0, 'Bounties': 1000000 if s else 0, 'CartData': 2000000 if s else 0, 'ExoData': 3000000 if s else 0, @@ -1260,12 +1261,13 @@ def _update_system_data(self, system_data:dict): if not 'tp' in system_data['TWSandR']: system_data['TWSandR']['tp'] = {'scooped': 0, 'delivered': 0} - def _update_faction_data(self, faction_data: Dict, faction_state: str = None): + def _update_faction_data(self, faction_data: dict, faction_state: str|None = None, faction_inf: float|None = None): """ Update faction data structure for elements not present in previous versions of plugin """ - # Update faction state as it can change at any time post-tick + # Update faction state and influence as it can change at any time post-tick if faction_state: faction_data['FactionState'] = faction_state + if faction_inf: faction_data['Influence'] = faction_inf # From < v1.2.0 to 1.2.0 if not 'SpaceCZ' in faction_data: faction_data['SpaceCZ'] = {} @@ -1314,6 +1316,8 @@ def _update_faction_data(self, faction_data: Dict, faction_state: str = None): faction_data['MissionPointsSecondary'] = {'1': 0, '2': 0, '3': 0, '4': 0, '5': 0, 'm': int(faction_data.get('MissionPointsSecondary', 0))} # From < 4.0.0 to 4.0.0 if not 'SandR' in faction_data: faction_data['SandR'] = {'dp': 0, 'op': 0, 'tp': 0, 'bb': 0, 'wc': 0, 'pe': 0, 'pp': 0, 'h': 0} + # From < 4.2.0 to 4.2.0 + if not 'Influence' in faction_data: faction_data['Influence'] = 0 def _is_faction_data_zero(self, faction_data: Dict): diff --git a/bgstally/windows/activity.py b/bgstally/windows/activity.py index 8ff666e..2ee2687 100644 --- a/bgstally/windows/activity.py +++ b/bgstally/windows/activity.py @@ -175,6 +175,7 @@ def show(self, activity: Activity): col: int = 1 ttk.Label(frm_table, text=_("Faction"), font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 # LANG: Activity window column title + ttk.Label(frm_table, text="%", font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 ttk.Label(frm_table, text=_("State"), font=FONT_HEADING_2).grid(row=0, column=col, padx=2, pady=2); col += 1 # LANG: Activity window column title lbl_inf: ttk.Label = ttk.Label(frm_table, text="INF", font=FONT_HEADING_2, anchor=tk.CENTER) # LANG: Activity window column title, abbreviation for influence lbl_inf.grid(row=0, column=col, columnspan=2, padx=2) @@ -272,6 +273,7 @@ def show(self, activity: Activity): settlement_row_index += 1 col = 2 + ttk.Label(frm_table, text="{0:.2f}".format(faction['Influence'] * 100)).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 ttk.Label(frm_table, text=faction['FactionState']).grid(row=x + header_rows, column=col, sticky=tk.N); col += 1 MissionPointsVar = tk.IntVar(value=faction['MissionPoints']['m']) ttk.Spinbox(frm_table, from_=-999, to=999, width=3, textvariable=MissionPointsVar).grid(row=x + header_rows, column=col, sticky=tk.N, padx=2, pady=2); col += 1 From 2436a7e27eb4e61fe82806138e8df5f999b5a4c5 Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 19:29:15 +0000 Subject: [PATCH 18/27] Order factions by %, highest first --- CHANGELOG.md | 1 + bgstally/activity.py | 14 +++++++++++++- bgstally/windows/activity.py | 4 +++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa405e0..cf02bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Added new Discord formatter supporting the Celestial Light Brigade's preferred Discord structure and layout for BGS reports. * Each faction now has its influence % shown in the on-screen activity window. +* Factions are shown ordered by % influence, highest first in the on-screen activity window. ### Changes: diff --git a/bgstally/activity.py b/bgstally/activity.py index 5ec0404..221b9f1 100644 --- a/bgstally/activity.py +++ b/bgstally/activity.py @@ -205,13 +205,25 @@ def get_title(self, discord:bool = False) -> str: return f"{str(self.tick_time.strftime(DATETIME_FORMAT_TITLE))} (" + (__("game", lang=self.bgstally.state.discord_lang) if discord else _("game")) + ")" # LANG: Appended to tick time if a normal tick - def get_ordered_systems(self): + def get_ordered_systems(self) -> list: """ Get an ordered list of the systems we are tracking, with the current system first, followed by those with activity, and finally those without """ return sorted(self.systems.keys(), key=lambda x: (str(x) != self.bgstally.state.current_system_id, self.systems[x]['zero_system_activity'], self.systems[x]['System'])) + def get_ordered_factions(self, factions: dict) -> list: + """Return the provided factions (values from the dict) as a list, ordered by influence highest first + + Args: + factions (dict): A dict containing the factions to order + + Returns: + list: An ordered list of factions + """ + return sorted(factions.values(), key = lambda x: x['Influence'], reverse = True) + + def get_current_system(self) -> dict | None: """ Get the data for the current system diff --git a/bgstally/windows/activity.py b/bgstally/windows/activity.py index 2ee2687..740557f 100644 --- a/bgstally/windows/activity.py +++ b/bgstally/windows/activity.py @@ -248,7 +248,9 @@ def show(self, activity: Activity): header_rows = 3 x = 0 - for faction in system['Factions'].values(): + faction_list: list = activity.get_ordered_factions(system['Factions']) + + for faction in faction_list: chk_enable = ttk.Checkbutton(frm_table) chk_enable.grid(row=x + header_rows, column=0, sticky=tk.N, padx=2, pady=2) chk_enable.configure(command=partial(self._enable_faction_change, nb_tab, tab_index, chk_enable_all, FactionEnableCheckbuttons, activity, system, faction, x)) From ec8ea50a139e28430b5532771c33cf6591f849f5 Mon Sep 17 00:00:00 2001 From: aussig Date: Tue, 29 Oct 2024 19:30:04 +0000 Subject: [PATCH 19/27] Remove old 2.2.0-alpha data migration code --- bgstally/activity.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/bgstally/activity.py b/bgstally/activity.py index 221b9f1..1b5aa17 100644 --- a/bgstally/activity.py +++ b/bgstally/activity.py @@ -1298,20 +1298,6 @@ def _update_faction_data(self, faction_data: dict, faction_state: str|None = Non if not 'Scenarios' in faction_data: faction_data['Scenarios'] = 0 # From < v2.2.0 to 2.2.0 if not 'TWStations' in faction_data: faction_data['TWStations'] = {} - # 2.2.0-a1 - 2.2.0-a3 stored a single integer for passengers, escapepods and cargo in TW station data. 2.2.0-a4 onwards has a dict for each. - # Put the previous values for passengers and escapepods into the 'm' 'sum' entries in the dict, for want of a better place. - # Put the previous value for cargo into the 'sum' entry in the dict. - # The previous mission count value was aggregate across all passengers, escape pods and cargo so just plonk in escapepods for want of a better place. - # We can remove all this code on release of final 2.2.0 - for station in faction_data['TWStations'].values(): - if not type(station.get('passengers')) == dict: - station['passengers'] = {'l': {'count': 0, 'sum': 0}, 'm': {'count': 0, 'sum': station['passengers']}, 'h': {'count': 0, 'sum': 0}} - if not type(station.get('escapepods')) == dict: - station['escapepods'] = {'l': {'count': 0, 'sum': 0}, 'm': {'count': station['missions'], 'sum': station['escapepods']}, 'h': {'count': 0, 'sum': 0}} - if not type(station.get('cargo')) == dict: - station['cargo'] = {'count': 0, 'sum': station['cargo']} - if not type(station.get('massacre')) == dict: - station['massacre'] = {'s': {'count': 0, 'sum': 0}, 'c': {'count': 0, 'sum': 0}, 'b': {'count': 0, 'sum': 0}, 'm': {'count': 0, 'sum': 0}, 'h': {'count': 0, 'sum': 0}, 'o': {'count': 0, 'sum': 0}} # From < 3.0.0 to 3.0.0 if not 'GroundMurdered' in faction_data: faction_data['GroundMurdered'] = 0 if not 'TradeBuy' in faction_data: From 9b9fa46aba66ed1612173f00bc7c70d359a4c0da Mon Sep 17 00:00:00 2001 From: aussig Date: Sat, 2 Nov 2024 20:41:54 +0000 Subject: [PATCH 20/27] Remove debug message. --- bgstally/activity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bgstally/activity.py b/bgstally/activity.py index 1b5aa17..7132d6b 100644 --- a/bgstally/activity.py +++ b/bgstally/activity.py @@ -723,7 +723,6 @@ def ship_targeted(self, journal_entry: dict, state: State): """ # Always clear last targeted on new target lock if journal_entry.get('TargetLocked', False) == True: - Debug.logger.info("Cleared last ship targeted") state.last_ship_targeted = {} if 'Faction' in journal_entry and 'PilotName_Localised' in journal_entry and 'PilotName' in journal_entry: From 251e4c0f1bc7d2bbc0bc530662808c6271470446 Mon Sep 17 00:00:00 2001 From: dwomble Date: Sun, 3 Nov 2024 01:13:34 -0700 Subject: [PATCH 21/27] Updated CLB Formatter (#280) * CLB formatter Code to format BGS reports the way we do for Celestial Light Brigade * Update CHANGELOG * Update clb.py Efficiency and layout improvements --------- Co-authored-by: aussig --- bgstally/formatters/clb.py | 158 +++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 76 deletions(-) diff --git a/bgstally/formatters/clb.py b/bgstally/formatters/clb.py index e05cfe3..27b9028 100644 --- a/bgstally/formatters/clb.py +++ b/bgstally/formatters/clb.py @@ -6,6 +6,7 @@ from bgstally.constants import CheckStates, DiscordActivity from bgstally.debug import Debug from bgstally.formatters.default import DefaultActivityFormatter +#from bgstally.formatters.base import FieldActivityFormatterInterface from bgstally.utils import _, __, human_format, is_number from thirdparty.colors import * @@ -30,7 +31,6 @@ def get_name(self) -> str: """ return 'Celestial Light Brigade' - def is_visible(self) -> bool: """Should this formatter be visible to the user as a choice. @@ -89,22 +89,22 @@ def get_fields(self, activity: Activity, activity_mode: DiscordActivity, system_ """ discord_fields = [] - for system in activity.systems.copy().values(): # Use a copy for thread-safe operation - if system_names is not None and system['System'] not in system_names: continue - system_text: str = "" + for system in sorted(activity.systems.copy().values(), key=lambda d: d['System']): # Use a copy for thread-safe operation + if system_names is not None and system['System'] not in system_names: + continue + system_text: str = "" if activity_mode == DiscordActivity.THARGOIDWAR or activity_mode == DiscordActivity.BOTH: system_text += self._build_tw_system(system, True, lang) if (activity_mode == DiscordActivity.BGS or activity_mode == DiscordActivity.BOTH) and system.get('tw_status') is None: - for faction in system['Factions'].values(): + for faction in sorted(system['Factions'].values(), key=lambda d: d['Faction']): if faction['Enabled'] != CheckStates.STATE_ON: continue system_text += self._build_faction(faction, True, lang) if system_text != "": - system_text = system_text.replace("'", "") - system_text = system_text.replace(" ", " ") - system_text = system_text.replace(" ", "") +# system_text = system_text.replace(" ", " ") +# system_text = system_text.replace(" ", "") discord_field = {'name': system['System'], 'value': f"```ansi\n{system_text}```"} discord_fields.append(discord_field) @@ -119,7 +119,8 @@ def _build_text(self, activity: Activity, activity_mode: DiscordActivity, system text:str = "" # Force plain text if we are not posting to Discord fp: bool = not discord - for system in activity.systems.copy().values(): # Use a copy for thread-safe operation + + for system in sorted(activity.systems.copy().values(), key=lambda d: d['System']): # Use a copy for thread-safe operation if system_names is not None and system['System'] not in system_names: continue @@ -129,29 +130,28 @@ def _build_text(self, activity: Activity, activity_mode: DiscordActivity, system system_text += self._build_tw_system(system, True, lang) if (activity_mode == DiscordActivity.BGS or activity_mode == DiscordActivity.BOTH) and system.get('tw_status') is None: - for faction in system['Factions'].values(): + for faction in sorted(system['Factions'].values(), key=lambda d: d['Faction']): if faction['Enabled'] != CheckStates.STATE_ON: continue system_text += self._build_faction(faction, DiscordActivity, lang) if system_text != "": - text += f" {color_wrap(system['System'], 'white', None, 'bold', fp=fp)}\n{system_text}" + text += f"\n {color_wrap(system['System'], 'white', None, 'bold', fp=fp)}\n{system_text}" if discord and activity.discord_notes is not None and activity.discord_notes != "": text += "\n" + activity.discord_notes offset = time.mktime(datetime.now().timetuple()) - time.mktime(datetime.now(timezone.utc).timetuple()) tick = round(time.mktime(activity.tick_time.timetuple()) + offset) - text = f"### {__('BGS Report', lang)} - {__('Tick', lang)} : \n```ansi\n{text}```" + text = f"### {__('BGS Report', lang)} - {__('Tick', lang)} : \n```ansi{text}```" #else: # text = "BGS Report - Tick : " + self.tick_time.strftime(DATETIME_FORMAT_TITLE) + "\n\n" + text - return text.replace("'", "") + return text except BaseException as error: return f"{traceback.format_exc()}\n An exception occurred: {error}" - - def _build_inf_text(self, inf_data: dict, secondary_inf_data: dict, faction_state: str, discord: bool, lang:str) -> str: + def _build_inf_text(self, inf_data: dict, secondary_inf_data: dict, faction_state: str, discord: bool, lang: str) -> str: """ Create a complete summary of INF for the faction, including both primary and secondary if user has requested @@ -170,28 +170,17 @@ def _build_inf_text(self, inf_data: dict, secondary_inf_data: dict, faction_stat if self.bgstally.state.secondary_inf: inf += sum((1 if k == 'm' else int(k)) * int(v) for k, v in secondary_inf_data.items()) + if faction_state in STATES_ELECTION: + type = __("Election Inf", lang) + elif faction_state in STATES_WAR: + type = __("War Inf", lang) + else: + type = __("Inf", lang) if inf > 0: - return f"{green('+' + str(inf), fp=fp)} {blue(__('Inf', lang), fp=fp)}" + return f"{green('+' + str(inf), fp=fp)} {type}" return "" - def _build_cz_text(self, cz_data: dict, prefix: str, discord: bool, lang: str) -> str: - """ - Create a summary of Conflict Zone activity - """ - if cz_data == {}: return "" - fp: bool = not discord - text:str = "" - - czs = [] - for w in ['h', 'm', 'l']: - if w in cz_data and int(cz_data[w]) != 0: - czs.append(f"{green(str(cz_data[w]), fp=fp)} {red(__(w.upper()+prefix, lang), fp=fp)}") - if len(czs) == 0: - return "" - - return ", ".join(czs) - def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: """ Generate formatted text for a faction @@ -199,33 +188,18 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: # Force plain text if we are not posting to Discord fp: bool = not discord - # Store the different item types in a list, we'll join them at the end to create a csv string + # Start with the main influence items. + activity = [] inf = self._build_inf_text(faction['MissionPoints'], faction['MissionPointsSecondary'], faction['FactionState'], discord, lang) if inf != "": activity.append(inf) - for t, d in {'SpaceCZ': 'SCZ', 'GroundCZ': 'GCZ'}.items(): - cz = self._build_cz_text(faction.get(t, {}), d, discord, lang) - if cz != "": - activity.append(cz) - - for action, desc in {'TradeBuy': 'Spend', 'TradeSell': "Profit"}.items(): - if sum(int(d['value']) for d in faction[action]) > 0: - if not self.bgstally.state.detailed_trade: - tot = 0 - for t, d in {0 : "[Z]", 1 : "[L]", 2 : "[M]", 3 : "[H]"}.items(): - if faction[action][t] and faction[action][t]['value'] > 0: - tot += faction[action][t]['value'] - activity.append(f"{human_format(tot)} {__(desc, lang)}") - else: - for t, d in {0 : "[Z]", 1 : "[L]", 2 : "[M]", 3 : "[H]"}.items(): - if faction[action][t] and faction[action][t]['value'] > 0: - activity.append(f"{human_format(faction[action][t]['value'])} {__(desc, lang)} {d} ({str(faction[action][t]['items'])}T)") - - # These are simple we can just loop through them - activities = {"Bounties" : "Bounties", + # For all the other high level actions just loop through these and sum them up. + actions = {"Bounties" : "Bounties", "CombatBonds" : "Bonds", + "SpaceCZ" : "SCZ", + "GroundCZ" : "GCZ", "BlackMarketProfit" : "BlackMarket", "CartData" : "Carto", "ExoData" : "Exo", @@ -233,52 +207,84 @@ def _build_faction(self, faction: dict, discord: bool, lang: str) -> str: "GroundMurdered" : "Foot Murders", "Scenarios" : "Scenarios", "MissionFailed" : "Failed", + 'TradeBuy': 'Spent', + 'TradeSell': "Profit", "SandR" : "S&R Units" } - for a in activities: + for a in actions: if faction.get(a): amt = 0 + # The total value depends on the data type. if isinstance(faction[a], int): amt = faction[a] + if isinstance(faction[a], list): + amt: int = sum(int(d['value']) for d in faction[a]) # Sum the value if isinstance(faction[a], dict): - amt: int = sum(int(v) for k, v in faction[a].items()) + amt: int = sum(int(v) for k, v in faction[a].items()) # Count the records if amt > 0: - activity.append(f"{green(human_format(amt), fp=fp)} {__(activities[a], lang)}") + activity.append(f"{green(human_format(amt), fp=fp)} {__(actions[a], lang)}") activity_discord_text = ', '.join(activity) # Now do the detailed sections if self.bgstally.state.detailed_inf: - if self.bgstally.state.secondary_inf: - for t, d in {'MissionPoints' : '[P]', 'MissionPointsSecondary' : '[S]'}.items(): - for i in range(1, 6): - if faction[t].get(str(i), 0) != 0: - activity_discord_text += grey(f"\n {faction[t][str(i)]} {d} {__('Inf', lang)}{'+' * i}", fp=fp) - else: - for i in range(1, 6): - if faction['MissionPoints'].get(str(i), 0) != 0: - activity_discord_text += grey(f"\n {faction['MissionPoints'][str(i)]} {__('Inf', lang)}{'+' * i}", fp=fp) - - # And the Search and Rescue details - if 'SandR' in faction: - for t, d in {'op': 'Occupied Escape Pod', 'dp' : 'Damaged Escape Pod', 'bb' : 'Black Box'}.items(): - if faction['SandR'][t]: - activity_discord_text += grey(f"\n {faction['SandR'][t]} {__(d, lang)}", fp=fp) + activity_discord_text += self._build_faction_details(faction, discord, lang) if activity_discord_text == "": return "" # Faction name and summary of activities faction_name = self._process_faction_name(faction['Faction']) - faction_discord_text = f" {color_wrap(faction_name, 'yellow', None, 'bold', fp=fp)}: {activity_discord_text}\n" + if faction['Faction'] == self.get_name(): + faction_discord_text = f" {yellow(faction_name, fp=fp)} : {activity_discord_text}\n" + else: + faction_discord_text = f" {blue(faction_name, fp=fp)} : {activity_discord_text}\n" + + return faction_discord_text + + def _build_faction_details(self, faction: dict, discord: bool, lang: str) -> str: + """ + Build the detailed faction information if required + """ + activity = [] + fp: bool = not discord - # Add ground settlement details further indented + # Breakdown of Space CZs + scz = faction.get('SpaceCZ') + for w in ['h', 'm', 'l']: + if w in scz and int(scz[w]) != 0: + activity.append(grey(f" {str(scz[w])} [{w.upper()}] {__('Space', lang)}")) + + # Details of Ground CZs so we know where folks have and haven't fought for settlement_name in faction.get('GroundCZSettlements', {}): if faction['GroundCZSettlements'][settlement_name]['enabled'] == CheckStates.STATE_ON: - faction_discord_text += grey(f" {faction['GroundCZSettlements'][settlement_name]['count']} {settlement_name}\n") + activity.append(grey(f" {faction['GroundCZSettlements'][settlement_name]['count']} [{faction['GroundCZSettlements'][settlement_name]['type'].upper()}] {__('Ground', lang)} - {settlement_name}")) + + # Trade details + if self.bgstally.state.detailed_trade: + for action, desc in {'TradeBuy': 'Spent', 'TradeSell': "Profit"}.items(): + for t, d in {3 : "H", 2 : "M", 1 : "L", 0 : "Z"}.items(): + if faction[action][t] and faction[action][t]['value'] > 0: + activity.append(grey(f" {human_format(faction[action][t]['value'])} [{d}] {__(desc, lang)} ({str(faction[action][t]['items'])}T)")) + + # Breakdown of mission influence + for t, d in {'MissionPoints' : 'P', 'MissionPointsSecondary' : 'S'}.items(): + if self.bgstally.state.secondary_inf or t != 'MissionPointsSecondary': + for i in range(1, 6): + if faction[t].get(str(i), 0) != 0: + activity.append(grey(f" {faction[t][str(i)]} [{d}] {__('Inf', lang)}{'+' * i}", fp=fp)) - return faction_discord_text + # Search and rescue, we treat this as detailed inf + if 'SandR' in faction: + for t, d in {'op': 'Occupied Escape Pod', 'dp' : 'Damaged Escape Pod', 'bb' : 'Black Box'}.items(): + if faction['SandR'][t]: + activity.append(grey(f" {faction['SandR'][t]} {__(d, lang)}", fp=fp)) + + if len(activity) == 0: + return "" + + return "\n" + "\n".join(activity) def _process_faction_name(self, faction_name): """ From b7e2549cf1b9a988c264bbb4b4eb2ddf7d66db6d Mon Sep 17 00:00:00 2001 From: Giampiero <54741923+GLWine@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:02:53 +0100 Subject: [PATCH 22/27] Fix regex pattern with raw string literals (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix regular expression syntax for ANSI color code filtering - Updated the regular expressions for detecting and filtering ANSI color codes. - Replaced the old string literals with raw string literals (prefix `r`) to avoid potential issues with escape sequences. - This change ensures the regular expressions are interpreted correctly by Python's `re` module, avoiding potential bugs caused by unescaped characters. - By applying this fix, we improve the robustness and readability of the regular expressions, ensuring they work reliably in all scenarios. Effects of not applying the modification: - Without this change, the regular expressions might not handle escape sequences properly, leading to incorrect matching or errors during pattern compilation. - This could cause failures in filtering out ANSI color codes, resulting in incorrect or garbled output in the application. * Fix regex pattern by using raw string literal - Updated the regular expression pattern in the `string_to_alphanumeric` function to use a raw string literal (prefix `r`). - This ensures that backslashes in the pattern are treated correctly, preventing potential issues with escape sequences. - By making this change, we ensure that the regular expression is interpreted as intended, avoiding any unexpected behavior when cleaning the string. Effects of not applying the modification: - Without this fix, the regular expression might not behave correctly due to escape sequences being misinterpreted by Python’s `re` module. - This could lead to errors in cleaning the string, potentially causing incorrect output or failure to properly remove non-alphanumeric characters. --- bgstally/utils.py | 2 +- bgstally/widgets.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bgstally/utils.py b/bgstally/utils.py index 16d023c..3796dfe 100644 --- a/bgstally/utils.py +++ b/bgstally/utils.py @@ -153,5 +153,5 @@ def string_to_alphanumeric(s: str) -> str: Returns: str: The cleaned string """ - pattern: re.Pattern = re.compile('[\W_]+') + pattern: re.Pattern = re.compile(r'[\W_]+') return pattern.sub('', s) diff --git a/bgstally/widgets.py b/bgstally/widgets.py index 22bf3c6..f865043 100644 --- a/bgstally/widgets.py +++ b/bgstally/widgets.py @@ -60,8 +60,8 @@ class to convert text with a limited set of Discord-supported ansi color codes t # define some regexes which will come in handy in filtering # out the ansi color codes - color_pat = re.compile("\x01?\x1b(\[[\d;]*m?)\x02?") - inner_color_pat = re.compile("^\[([\d;]*)m$") + color_pat = re.compile(r'\x01?\x1b(\[[\d;]*m?)\x02?') + inner_color_pat = re.compile(r'^\[([\d;]*)m$') def __init__(self, *args, **kwargs): """ From 662e5706c151fe802252cc4d71304bd8f625aa30 Mon Sep 17 00:00:00 2001 From: Giampiero <54741923+GLWine@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:04:46 +0100 Subject: [PATCH 23/27] Fix: Use tuple for padding in Tkinter grid (#283) In the 'WindowCMDRs' class, corrected padding values for labels by converting lists into tuples. This resolves the issue where the 'pady' parameter was passed as a list instead of a tuple, ensuring compatibility with Tkinter's grid system. Changes made: - Fixed 'pady' usage in 'CMDR Details' and 'Interaction' labels. --- bgstally/windows/cmdrs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bgstally/windows/cmdrs.py b/bgstally/windows/cmdrs.py index fdc70ea..9cef927 100644 --- a/bgstally/windows/cmdrs.py +++ b/bgstally/windows/cmdrs.py @@ -72,7 +72,7 @@ def show(self): treeview.pack(fill=tk.BOTH, expand=1) current_row = 0 - ttk.Label(frm_details, text=_("CMDR Details"), font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).grid(row=current_row, column=0, sticky=tk.W, columnspan=4, padx=5, pady=[5, 0]); current_row += 1 # LANG: Label on CMDR window + ttk.Label(frm_details, text=_("CMDR Details"), font=FONT_HEADING_1, foreground=COLOUR_HEADING_1).grid(row=current_row, column=0, sticky=tk.W, columnspan=4, padx=5, pady=(5, 0)); current_row += 1 # LANG: Label on CMDR window ttk.Label(frm_details, text=_("Name: "), font=FONT_HEADING_2).grid(row=current_row, column=0, sticky=tk.W, padx=5) # LANG: Label on CMDR window self.lbl_cmdr_details_name: ttk.Label = ttk.Label(frm_details, text="", width=50) self.lbl_cmdr_details_name.grid(row=current_row, column=1, sticky=tk.W, padx=5) @@ -87,7 +87,7 @@ def show(self): self.lbl_cmdr_details_squadron_inara.grid(row=current_row, column=3, sticky=tk.W, padx=5); current_row += 1 ttk.Label(frm_details, text=_("Interaction: "), font=FONT_HEADING_2).grid(row=current_row, column=0, sticky=tk.W, padx=5) # LANG: Label on CMDR window self.lbl_cmdr_details_interaction: ttk.Label = ttk.Label(frm_details, text="") - self.lbl_cmdr_details_interaction.grid(row=current_row, column=1, sticky=tk.W, padx=5, pady=[0, 5]); current_row += 1 + self.lbl_cmdr_details_interaction.grid(row=current_row, column=1, sticky=tk.W, padx=5, pady=(0, 5)); current_row += 1 for column in column_info: treeview.heading(column['title'], text=column['title'].title(), sort_by=column['type']) From 2de9246e5b3ad323c74cff2516b75e1a9c757459 Mon Sep 17 00:00:00 2001 From: aussig Date: Mon, 16 Dec 2024 15:57:45 +0000 Subject: [PATCH 24/27] Change Combat to Conflict --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf02bf9..8d8d367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -610,7 +610,7 @@ _* Note that the plugin only tracks primary and secondary INF from this version ### New features: -* Ability to manually add High, Medium and Low on-foot and in-space Combat Zone wins to the Discord report by clicking on-screen buttons. +* Ability to manually add High, Medium and Low on-foot and in-space Conflict Zone wins to the Discord report by clicking on-screen buttons. ### Changes: From 7cda090933ae521ea611377493b8bd6d3f21b1ab Mon Sep 17 00:00:00 2001 From: aussig Date: Sun, 22 Dec 2024 15:44:38 +0000 Subject: [PATCH 25/27] Hungarian Translation Fixes #281 --- CHANGELOG.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8d367..8b36108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Added new Discord formatter supporting the Celestial Light Brigade's preferred Discord structure and layout for BGS reports. * Each faction now has its influence % shown in the on-screen activity window. * Factions are shown ordered by % influence, highest first in the on-screen activity window. +* Added Hungarian translation. ### Changes: diff --git a/README.md b/README.md index fa3cf86..219604a 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ And finally, a huge thank you to: * All the translators who have given their time and effort: * French - CMDR Dopeilien and CMDR ThArGos * German - CMDR Ryan Murdoc + * Hungarian - CMDR Lazy Creature and CMDR xtl * Italian - CMDR FrostBit / [@GLWine](https://github.com/GLWine) * Portuguese (Portugal) - CMDR Holy Nothing * Portuguese (Brazil) - CMDR FelaKuti From 0d7849cb93c11f687fe4dce1dd04b3acc718fb67 Mon Sep 17 00:00:00 2001 From: Aussi Date: Sun, 22 Dec 2024 18:12:39 +0000 Subject: [PATCH 26/27] New translations en.template.strings (Hungarian) (#285) --- L10n/hu.strings | 863 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 863 insertions(+) create mode 100644 L10n/hu.strings diff --git a/L10n/hu.strings b/L10n/hu.strings new file mode 100644 index 0000000..28f4480 --- /dev/null +++ b/L10n/hu.strings @@ -0,0 +1,863 @@ +/* Language name */ +"!Language" = "Magyar"; + +/* [legend.py] */ +"(Cannot be automatically distinguished)" = "(Nem sikerült automatikusan megállapítani)"; + +/* Discord text. [fleetcarrier.py] */ +"A carrier jump has been scheduled" = "Egy anyahajó ugrás naptárazva"; + +/* Preferences checkbox label. [ui.py] */ +"Abbreviate Faction Names" = "Frakció nevek rövidítése"; + +/* Label on API settings window. [api.py] */ +"About This" = "Erről"; + +/* Preferences checkbox label. [ui.py] */ +"Activity Indicator" = "Tevékenység jelző"; + +/* Label on activity window. [activity.py] */ +"Activity to post:" = "Közvetítendő tevékenység:"; + +/* CMDR information. [targetmanager.py] */ +"Added a friend" = "Barát hozzáadva"; + +/* Preferences heading. [ui.py] */ +"Advanced" = "Bővebb keresés"; + +/* Discord carrier docking access. [fleetcarrier.py] */ +"All" = "Összes"; + +/* Main window label. [ui.py] */ +"API changed, open settings to re-approve" = "API megváltozott, nyisd ki a beállításokat, hogy ismét engedélyezd"; + +/* Label on API settings window. [api.py] */ +"API Information" = "API információ"; + +/* Label on API settings window. [api.py] */ +"API Key" = "API kulcs"; + +/* Label on API settings window. [api.py] */ +"API Settings" = "API Beállítások"; + +/* Label on API settings window. [api.py] */ +"Approved by you" = "Ön által engedélyezve"; + +/* Preferences force tick text. [ui.py] */ +"Are you sure that you want to do this?" = "Biztos vagy benne, hogy ezt akarod?"; + +/* Text on API settings window. [api.py] */ +"Ask the server administrator for the information below, then click 'Establish Connection' to continue. Buttons to pre-fill some information for popular servers are provided, but you will need to enter your API key which is unique to you." = "A lent említett információért fordulj a szerver adminisztrátorhoz majd nyomb meg \"Kapcsolat létrehozása\" gombot a fojtatáshoz. Népszerú szerverekhez vannak előre készített gombok, de a saját egyedi API kulcsodat meg kell adnod."; + +/* Discord heading, abbreviation for black box. [default.py] */ +"bb" = "bb"; + +/* Discord post title. [activity.py] */ +"BGS Activity after Tick: {tick_time}" = "BGS Tevékenység Tick után: {tick_time}"; + +/* Dropdown menu on activity window. [activity.py] */ +"BGS Only" = "BGS Kizárólag"; + +/* Discord heading. [default.py] */ +"bio-pod" = "bio-pod"; + +/* Activity window tooltip. [activity.py] */ +"Black market profit" = "Fekete piaci profit"; + +/* Activity window column title, abbreviation for black market profit. [activity.py] */ +"BM Prof" = "FP profit"; + +/* Dropdown menu on activity window. [activity.py] */ +"Both BGS and TW" = "BGS és TW egyben"; + +/* Dropdown menu on activity window. [fleetcarrier.py] */ +"Both Materials and Commodities" = "Anyagok és árucikkek"; + +/* Activity window tooltip. [activity.py] */ +"Bounty vouchers" = "Vérdíjak"; + +/* Label on carrier window. [fleetcarrier.py] */ +"Buying Commodities" = "Árúcikket Vásárol"; + +/* Discord fleet carrier section heading. [fleetcarrier.py] */ +"Buying Commodities:" = "Árúcikket Vásárol:"; + +/* Label on carrier window. [fleetcarrier.py] */ +"Buying Materials" = "Anyagot vásárol"; + +/* Discord fleet carrier section heading. [fleetcarrier.py] */ +"Buying Materials:" = "Anyagot vásárol:"; + +/* Discord heading. [default.py] */ +"cargo" = "rakomány"; + +/* Label on legend window. [legend.py] */ +"Cargo missions" = "Rakomány küldetések"; + +/* Discord fleet carrier title. [fleetcarrier.py] */ +"Carrier {carrier_name}" = "Anyahajó {carrier_name}"; + +/* [sheet_options.py] */ +"Clear contents" = "Tartalom törlése"; + +/* Label on CMDR window. [cmdrs.py] */ +"CMDR Details" = "CMDR Részletek"; + +/* Discord heading. [cmdrs.py] */ +"CMDR Inara Link" = "CMDR Inara Link"; + +/* Preferences checkbox label. [ui.py] */ +"CMDR Info" = "CMDR Info"; + +/* Activity window tooltip. [activity.py] */ +"Combat bonds" = "Harci kötvények"; + +/* Dropdown menu on activity window. [fleetcarrier.py] */ +"Commodities Only" = "Csak Árúcikkek"; + +/* Preferences button label. [ui.py] */ +"Configure Remote Server" = "Távoli Szerver Konfigurálása"; + +/* Preferences force tick popup title. [ui.py] */ +"Confirm Force a New Tick" = "Erősítse meg Új Tick kényszerítését"; + +/* [widgets.py] */ +"Copy" = "Másolás"; + +/* [sheet_options.py] */ +"Copy contents" = "Tartalom másolása"; + +/* Button label. [activity.py] */ +"Copy to Clipboard" = "Másolás a vágólapra"; + +/* Discord heading, abbreviation for critically injured. [default.py] */ +"crit" = "krit"; + +/* Label on legend window. [legend.py] */ +"Critically wounded evacuation missions" = "Kritikusan sérültet evakuáló küldetés"; + +/* [ui.py] */ +"Curr Tick:" = "Jelen Tick:"; + +/* [fleetcarrier.py] */ +"Current System" = "Jelenlegi Rendszer"; + +/* Preferences checkbox label. [ui.py] */ +"Current Tick" = "Jelenlegi Tick"; + +/* [widgets.py] */ +"Cut" = "Kivágás"; + +/* [sheet_options.py] */ +"Cut contents" = "Tartalom kivágása"; + +/* CMDR window column title. [cmdrs.py] */ +"Date / Time" = "Dátum / Idő"; + +/* Discord heading. [cmdrs.py] */ +"Date and Time" = "Dátum és idő"; + +/* [ui.py] */ +"Default" = "Alapértelmezett"; + +/* [sheet_options.py] */ +"Delete" = "Törlés"; + +/* [sheet_options.py] */ +"Delete columns" = "Oszlopok törlése"; + +/* [sheet_options.py] */ +"Delete rows" = "Sorok törlése"; + +/* Button on CMDR window. [cmdrs.py] */ +"Delete Selected" = "Kiválasztott törlése"; + +/* [demo.py] */ +"Demo Data Only" = "Csak demo adat"; + +/* Discord heading. [fleetcarrier.py] */ +"Departure Time" = "Indulás ideje"; + +/* Label on API settings window. [api.py] */ +"Description" = "Leírás"; + +/* Label on legend window. [legend.py] */ +"Detailed INF split into + / ++ / +++ / ++++ / +++++ received from missions." = "Részletes INF elosztva + / ++ / +++ / ++++ / +++++ kapott küldetésekből."; + +/* Label on activity window. [activity.py] */ +"Discord Additional Notes" = "További Discord jegyzetek"; + +/* [ui.py] */ +"Discord Options" = "Discord opciók"; + +/* Label on activity window. [activity.py] */ +"Discord Report Preview ⓘ" = "Discord riport előnézet"; + +/* Preferences heading. [ui.py] */ +"Discord Webhooks" = "Discord webhookok"; + +/* Discord heading. [fleetcarrier.py] */ +"Docking" = "Dokkolás"; + +/* [activity.py] */ +"Double-check on-ground CZ tallies, sizes are not always correct" = "Ellenőrizd a felszíni CZ tally-t, a méretek nem mindig helyesek"; + +/* [sheet_options.py] */ +"Edit cell" = "Cella szerkesztése"; + +/* [sheet_options.py] */ +"Edit header" = "Fejléc szerkesztése"; + +/* [sheet_options.py] */ +"Edit index" = "Index szerkesztése"; + +/* Discord heading, abbreviation for election INF. [default.py] */ +"ElectionINF" = "Választási INF"; + +/* Label on activity window. [activity.py] */ +"Empty System, no BGS Activity Available" = "Üres rendszer, BGS tevékenység nem elérhető"; + +/* Activity window tooltip. [activity.py] */ +"Enable / disable all factions" = "Minden frakció engedélyezése / letiltása"; + +/* Activity window tooltip. [activity.py] */ +"Enable / disable faction" = "Frakció engedélyezése / letiltása"; + +/* Checkbox on API settings window. [api.py] */ +"Enable {activities_url} Requests" = "{activities_url} kérelmek engedélyezése"; + +/* Checkbox on API settings window. [api.py] */ +"Enable {events_url} Requests" = "{events_url} kérelmek engedélyezése"; + +/* Discord heading, abbreviation for escape pod. [default.py] */ +"esc-pod" = "Mentőkapszula"; + +/* Label on API settings window. [api.py] */ +"Establish a connection" = "Kapcsolat létrehozása"; + +/* Button on API settings window. [api.py] */ +"Establish Connection" = "Kapcsolat létrehozása"; + +/* Label on API settings window. [api.py] */ +"Events Requested" = "Események kérelmezve"; + +/* Discord heading, abbreviation for exobiology. [default.py] */ +"Exo" = "Exo"; + +/* Discord heading, abbreviation for exploration. [default.py] */ +"Expl" = "Felf"; + +/* Activity window tooltip. [activity.py] */ +"Exploration data" = "Felfedezési adat"; + +/* Activity window column title. [activity.py] */ +"Faction" = "Frakció"; + +/* Discord heading, abbreviation for failed missions. [default.py] */ +"Fails" = "Sikertelen"; + +/* Preferences table heading, abbreviation for fleet carrier commodities / materials. [ui.py] */ +"FC C/M" = "FC piac"; + +/* Preferences table heading, abbreviation for fleet carrier operations. [ui.py] */ +"FC Ops" = "FC műv"; + +/* Preferences button label. [ui.py] */ +"Force Tick" = "Tick kényszerítése"; + +/* Appended to tick time if a forced tick. [activity.py] */ +"forced" = "kényszerítve"; + +/* Preferences label. [ui.py] */ +"Format for Discord Posts" = "Discord üzenetek formátuma"; + +/* Discord CMDR information. [targetmanager.py] */ +"Friend request received from this CMDR" = "Barátkérelem érkezett ettől a CMDR-től"; + +/* Discord carrier docking access. [fleetcarrier.py] */ +"Friends" = "Barátok"; + +/* Discord heading. [fleetcarrier.py] */ +"From System" = "Indulási rendszer"; + +/* Appended to tick time if a normal tick. [activity.py] */ +"game" = "játék"; + +/* Preferences heading. [ui.py] */ +"General Options" = "Általános opciók"; + +/* Activity window column title. [activity.py] */ +"Ground" = "Felszín"; + +/* Activity window tooltip. [activity.py] */ +"Ground conflict zones" = "Felszíni konfliktus zónák"; + +/* Discord heading, abbreviation for ground conflict zones. [default.py] */ +"GroundCZs" = "FelszíniCZ-k"; + +/* Discord heading. [default.py] */ +"GroundMurders" = "FelszíniGyilk"; + +/* Activity window tooltip. [activity.py] */ +"High" = "Magas"; + +/* Button on API settings window. [api.py] */ +"I Approve" = "Jóváhagyom"; + +/* Button on API settings window. [api.py] */ +"I Do Not Approve" = "Nem hagyom jóvá"; + +/* Discord CMDR information. [targetmanager.py] */ +"I scanned this CMDR" = "Szkenneltem ezt a CMDR-t"; + +/* Heading on legend window. [legend.py] */ +"Icons in BGS Reports" = "Ikonok a BGS riportokban"; + +/* Heading on legend window. [legend.py] */ +"Icons in Thargoid War Reports" = "Ikonok a Thargoid Háború riportokban"; + +/* Discord heading. [cmdrs.py] */ +"In Ship" = "Hajóban"; + +/* Overlay CMDR information report message. [ui.py] */ +"In ship: {ship}" = "Hajó: {ship}"; + +/* Discord heading. [cmdrs.py] */ +"In Squadron" = "Squadronban"; + +/* Discord heading. [cmdrs.py] */ +"In System" = "Rendszerben"; + +/* Overlay CMDR information report message. [ui.py] */ +"In system: {system}" = "Rendszer: {system}"; + +/* Preferences heading. [ui.py] */ +"In-game Overlay" = "Játék átfedés"; + +/* Preferences label. [ui.py] */ +"In-game overlay support requires the separate EDMCOverlay plugin to be installed - see the instructions for more information." = "A játék átfedés támogatásához a különálló EDMCOverlay beépülő telepítése szükséges - további információkért lásd az instrukciókat."; + +/* Label on legend window. [legend.py] */ +"In-space Conflict Zone Side Objective: Cap ship" = "Űr konfliktus zóna mellékcél: Csatahajó"; + +/* Label on legend window. [legend.py] */ +"In-space Conflict Zone Side Objective: Enemy captain" = "Űrbeli konfliktus zóna mellékcél: Ellenséges kapitány"; + +/* Label on legend window. [legend.py] */ +"In-space Conflict Zone Side Objective: Propaganda wing" = "Űrbeli konfliktus zóna mellékcél: Propaganda osztag"; + +/* Label on legend window. [legend.py] */ +"In-space Conflict Zone Side Objective: Spec ops wing" = "Űrbeli konfliktus zóna mellékcél: Különleges osztag"; + +/* Inara URL on CMDR window. [cmdrs.py] */ +"Inara Info Available ⤴" = "Inara info elérhető"; + +/* Overlay CMDR information report message. [ui.py] */ +"INARA INFORMATION AVAILABLE" = "INARA INFO ELÉRHETŐ"; + +/* Inara link. [activity.py] */ +"Inara ⤴" = "Inara ⤴"; + +/* Label on CMDR window. [cmdrs.py] */ +"Inara: " = "Inara: "; + +/* Checkbox label. [activity.py] */ +"Include" = "Tartalmaz"; + +/* Preferences checkbox label. [ui.py] */ +"Include Secondary INF" = "Tartalmaz másodlagos INF-et"; + +/* Discord heading, abbreviation for INF. [default.py] */ +"INF" = "INF"; + +/* Activity window tooltip. [activity.py] */ +"Influence" = "Befolyás"; + +/* Discord heading. [default.py] */ +"injured" = "sebesült"; + +/* Label on legend window. [legend.py] */ +"Injured evacuation missions" = "Sérült evakuáció küldetések"; + +/* [sheet_options.py] */ +"Insert column" = "Oszlop beszúrása"; + +/* [sheet_options.py] */ +"Insert columns left" = "Oszlop beszúrása balra"; + +/* [sheet_options.py] */ +"Insert columns right" = "Oszlop beszúrása jobbra"; + +/* [sheet_options.py] */ +"Insert row" = "Sor beszúrása"; + +/* [sheet_options.py] */ +"Insert rows above" = "Sor beszúrása fölé"; + +/* [sheet_options.py] */ +"Insert rows below" = "Sor beszúrása alá"; + +/* Preferences label. [ui.py] */ +"Instructions for Use" = "Használati útmutató"; + +/* Preferences heading. [ui.py] */ +"Integrations" = "Integráció"; + +/* CMDR window column title. [cmdrs.py] */ +"Interaction" = "Interakció"; + +/* Label on CMDR window. [cmdrs.py] */ +"Interaction: " = "Interakció: "; + +/* CMDR information. [targetmanager.py] */ +"Interdicted by" = "Eltérítve"; + +/* Discord CMDR information. [targetmanager.py] */ +"INTERDICTED BY this CMDR" = "ELTÉRÍTVE CMDR által"; + +/* Discord post title. [fleetcarrier.py] */ +"Jump Cancelled for Carrier {carrier_name}" = "Ugrás törölve {carrier_name} anyahajónak"; + +/* Discord post title. [fleetcarrier.py] */ +"Jump Scheduled for Carrier {carrier_name}" = "Ugrás naptárazva {carrier_name} anyahajónak"; + +/* CMDR information. [targetmanager.py] */ +"Killed by" = "Megölt"; + +/* Discord CMDR information. [targetmanager.py] */ +"KILLED BY this CMDR" = "Megölt CMDR"; + +/* Discord heading. [default.py] */ +"kills" = "ölések"; + +/* [legend.py] */ +"Kills" = "Ölések"; + +/* Preferences label. [ui.py] */ +"Language for Discord Posts" = "Discord üzenetek nyelve"; + +/* Main window label. [ui.py] */ +"Last BGS Tick:" = "Utolsó BGS Tick:"; + +/* Button label. [ui.py] */ +"Latest BGS Tally" = "Legutóbbi BGS összegzés"; + +/* CMDR window column title. [cmdrs.py] */ +"Legal" = "Legális"; + +/* Discord heading. [cmdrs.py] */ +"Legal Status" = "Legalitás"; + +/* Overlay CMDR information report message. [ui.py] */ +"Legal status: {legal}" = "Legalitás helyzete: {legal}"; + +/* Activity window tooltip. [activity.py] */ +"Low" = "Alacsony"; + +/* [legend.py] */ +"Massacre missions" = "Mészároló küldetések"; + +/* Dropdown menu on activity window. [fleetcarrier.py] */ +"Materials Only" = "Csak Anyagok"; + +/* Activity window tooltip. [activity.py] */ +"Medium" = "Közepes"; + +/* Discord CMDR information. [targetmanager.py] */ +"Message received from this CMDR in local chat" = "Üzenet fogadba CMDR-től lokális csaten"; + +/* Activity window tooltip. [activity.py] */ +"Mission fails" = "Sikertelen küldetések"; + +/* Discord heading. [default.py] */ +"missions" = "küldetések"; + +/* Discord heading, abbreviation for massacre (missions). [default.py] */ +"mm" = "mm"; + +/* Discord heading. [default.py] */ +"Murders" = "Gyilkosságok"; + +/* Label on API settings window. [api.py] */ +"Name" = "Név"; + +/* Label on CMDR window. [cmdrs.py] */ +"Name: " = "Név: "; + +/* Overlay message. [bgstally.py] */ +"NEW TICK DETECTED!" = "ÚJ TICK ÉSZLELVE!"; + +/* Preferences table heading. [ui.py] */ +"Nickname" = "Felhasználónév"; + +/* [fleetcarrier.py] */ +"No" = "Nem"; + +/* Discord carrier docking access. [fleetcarrier.py] */ +"None" = "Senki"; + +/* Discord heading. [fleetcarrier.py] */ +"Notorious Access" = "Visszaesők is"; + +/* Label on legend window. [legend.py] */ +"On-ground Conflict Zone" = "Felszíni konfliktus zóna"; + +/* [ui.py] */ +"Panels" = "Panelek"; + +/* Discord heading, abbreviation for passengers. [default.py] */ +"passeng" = "utasok"; + +/* Label on legend window. [legend.py] */ +"Passenger missions" = "Utas küldetések"; + +/* [ui.py] */ +"Past Estimated Tick Time" = "Túl a tick becsült idején"; + +/* [widgets.py] */ +"Paste" = "Beilleszt"; + +/* Checkbox label. [activity.py] */ +"Pin {system_name} to Overlay" = "{system_name} kitűzése az Átfedésre"; + +/* [ui.py] */ +"Pinned Systems" = "Kitűzött rendszerek"; + +/* URL label on API settings window. [api.py] */ +"Player Journal Documentation" = "Játékosnapló dokumentáció"; + +/* Text on API settings window. [api.py] */ +"PLEASE ENSURE YOU TRUST the server you send this information to!" = "GYŐZŐDJ MEG RÓLA, hogy MEGBÍZOL a szerverben, ahová ezt az információt elküldöd!"; + +/* Button on CMDR window. [cmdrs.py] */ +"Post CMDR List to Discord" = "CMDR lista küldése Discordra"; + +/* Button on CMDR window. [cmdrs.py] */ +"Post CMDR to Discord" = "CMDR küldése Discordra"; + +/* Button label. [activity.py] */ +"Post to Discord" = "Küldés Discordra"; + +/* Preferences label. [ui.py] */ +"Post to Discord as" = "Küldés Discordra mint"; + +/* Discord message footer, legacy text mode. [discord.py] */ +"Posted at: {date_time} | {plugin_name} v{version}" = "Küldve: {date_time} | {plugin_name} v{version}"; + +/* Button label. [ui.py] */ +"Previous BGS Tallies" = "Előző BGS tally-k"; + +/* Activity window column title, abbreviation for primary. [activity.py] */ +"Pri" = "Els."; + +/* Activity window tooltip. [activity.py] */ +"Primary" = "Elsődleges"; + +/* Label on legend window. [legend.py] */ +"Primary INF. This is INF gained for the mission issuing faction." = "Elsődleges INF: A küldetést kiadó frakció számára szerzett INF."; + +/* Activity window column title, abbreviation for profit. [activity.py] */ +"Prof" = "Prof."; + +/* Activity window tooltip for profit at zero | low | medium | high demand. [activity.py] */ +"Profit at Z | L | M | H demand" = "Profit Z | L | M | H kereslet esetén"; + +/* Activity window column title, abbreviation for purchase. [activity.py] */ +"Purch" = "Vás."; + +/* Activity window tooltip for purchase at low | medium | high supply. [activity.py] */ +"Purchase at L | M | H supply" = "Vásárlás L | M | H kínálat esetén"; + +/* Discord heading, abbreviation for reactivation (TW missions). [default.py] */ +"reac" = "reakt."; + +/* Label on legend window. [legend.py] */ +"Reactivation missions" = "Reaktivációs küldetések"; + +/* CMDR information. [targetmanager.py] */ +"Received friend request from" = "Barát kérés érkezett tőle"; + +/* CMDR information. [targetmanager.py] */ +"Received message from" = "Üzenet érkezett tőle"; + +/* CMDR information. [targetmanager.py] */ +"Received team invite from" = "Team meghívás érkezett tőle"; + +/* Preferences checkbox label. [ui.py] */ +"Report Newly Visited System Activity By Default" = "Újonnan meglátogatott rendszer jelentése alapértelmezés szerint"; + +/* Discord heading, abbreviation for search and rescue. [default.py] */ +"SandR" = "Kut&Ment."; + +/* CMDR information. [targetmanager.py] */ +"Scanned" = "Szkennelve"; + +/* Activity window tooltip. [activity.py] */ +"Scenario wins" = "Szcenárió győzelmek"; + +/* Discord heading. [default.py] */ +"Scenarios" = "Szcenáriók"; + +/* Activity window column title, abbreviation for scenarios. [activity.py] */ +"Scens" = "Szcen."; + +/* Label on legend window. [legend.py] */ +"Search & Rescue Bio Pods" = "Kutatás & Mentés: biopodok"; + +/* Label on legend window. [legend.py] */ +"Search & Rescue Black Boxes" = "Kutatás & Mentés: feketedobozok"; + +/* Label on legend window. [legend.py] */ +"Search & Rescue Escape Pods" = "Kutatás & Mentés: mentőkapszulák"; + +/* Label on legend window. [legend.py] */ +"Search & Rescue Tissue Samples" = "Kutatás & Mentés: szövetminták"; + +/* Activity window tooltip. [activity.py] */ +"Search and rescue" = "Kutatás & Mentés"; + +/* Activity window column title, abbreviation for secondary. [activity.py] */ +"Sec" = "Másod."; + +/* Activity window tooltip. [activity.py] */ +"Secondary" = "Másodlagos"; + +/* Label on legend window. [legend.py] */ +"Secondary INF. This is INF gained as a secondary effect of the mission, for example the destination faction for delivery missions." = "Másodlagos INF: A küldetés másodlagos hatásaként szerzett INF, például szállítási küldetés során a cél frakció számára."; + +/* [widgets.py] */ +"Select all" = "Mind kiválasztása"; + +/* Label on carrier window. [fleetcarrier.py] */ +"Selling Commodities" = "Eladó árucikkek"; + +/* Discord fleet carrier section heading. [fleetcarrier.py] */ +"Selling Commodities:" = "Eladó árucikkek:"; + +/* Label on carrier window. [fleetcarrier.py] */ +"Selling Materials" = "Eladó anyagok"; + +/* Discord fleet carrier section heading. [fleetcarrier.py] */ +"Selling Materials:" = "Eladó anyagok:"; + +/* Label on API settings window. [api.py] */ +"Server URL" = "Szerver URL"; + +/* Discord heading. [default.py] */ +"settlements" = "települések"; + +/* Activity window column title. [activity.py] */ +"Ship" = "Hajó"; + +/* Label on API settings window. [api.py] */ +"Shortcuts for Popular Servers" = "Gyorsgombok népszerű szerverekhez"; + +/* Main window tooltip. [ui.py] */ +"Show CMDR information window" = "Mutass CMDR információ ablakot"; + +/* Preferences checkbox label. [ui.py] */ +"Show Detailed INF" = "Mutass részletes INF"; + +/* Preferences checkbox label. [ui.py] */ +"Show Detailed Trade" = "Mutass részletes cserét"; + +/* Main window tooltip. [ui.py] */ +"Show fleet carrier window" = "Mutass anyahajó ablakot"; + +/* Preferences checkbox label. [ui.py] */ +"Show In-game Overlay" = "Mutass játékbeli Átfedőt"; + +/* Activity window tooltip. [activity.py] */ +"Show legend window" = "Mutass legend ablakot"; + +/* Preferences checkbox label. [ui.py] */ +"Show Systems with Zero Activity" = "Mutass rendszert nulla aktivitással"; + +/* [fleetcarrier.py] */ +"Some information cannot be updated. Enable Fleet Carrier CAPI Queries in File -> Settings -> Configuration" = "Bizonyos információ frissítése sikertelen volt. Engedéjezd Anyahajó CAPI lekérdezéseket Fájl -> Beállítások -> Konfiguráció"; + +/* Activity window tooltip. [activity.py] */ +"Space conflict zones" = "Űrbéli konfliktus zónák"; + +/* Discord heading, abbreviation for space conflict zones. [default.py] */ +"SpaceCZs" = "ŰrKZk"; + +/* Discord carrier docking access. [fleetcarrier.py] */ +"Squadron and Friends" = "Osztag és Barátok"; + +/* CMDR window column title. [cmdrs.py] */ +"Squadron ID" = "Osztag ID"; + +/* Overlay CMDR information report message. [ui.py] */ +"Squadron ID: {squadron}" = "Osztag ID: {squadron}"; + +/* Discord heading. [cmdrs.py] */ +"Squadron Inara Link" = "Osztag Inara link"; + +/* Label on CMDR window. [cmdrs.py] */ +"Squadron: " = "Osztag: "; + +/* Activity window column title. [activity.py] */ +"State" = "Állapot"; + +/* CMDR window column title. [cmdrs.py] */ +"System" = "Rendszer"; + +/* Discord heading. [default.py] */ +"System activity" = "Rendszer aktivitás"; + +/* Preferences checkbox label. [ui.py] */ +"System Information" = "Rendszerinformáció"; + +/* Label on carrier window. [fleetcarrier.py] */ +"System: {current_system} - Docking: {docking_access} - Notorious Allowed: {notorious}" = "Rendszer: {current_system} - Dokkolás: {docking_access} - Kőrözött engedve: {notorious}"; + +/* Text on API settings window. [api.py] */ +"Take care when agreeing to this - if you approve this server, {plugin_name} will send your information to it, which will include CMDR details such as your location, missions and kills." = "Nézd át alaposan, mielött beleegyezel ebbe - Ha engedélyezed ezt a szervert, {plugin_name} elfogja küldeni az információdat, ami tartalmaz CMDR részleteket, mint tartózkodási hely, küldetések és ölések."; + +/* Overlay message. [activity.py] */ +"Targeted Ally!" = "Célbavett szövetséges!"; + +/* Discord CMDR information. [targetmanager.py] */ +"Team invite received from this CMDR" = "Csapat meghívó érkezett CMDR-től"; + +/* Name of default output formatter. [text.py] */ +"Text Only" = "Csak szöveg"; + +/* Preferences checkbox label. [ui.py] */ +"Thargoid War Progress" = "Thargoid háború állapota"; + +/* Label on activity window. [activity.py] */ +"Thargoid War System, no BGS Activity is Counted" = "Thargoid Háború rendszer, BGS aktivitás nincs nyilvántartva"; + +/* Text on API settings window. [api.py] */ +"The exact set of Events that will be sent is listed in the 'Events Requested' section below. Further information about these Events and what they contain is provided here: " = "Az elküldésre kerülő Események listája megtekinthető lent a \"Események kérelmezve\" szakaszban. További információ ezekről az Eseményekről, és hogy mit tartalmaznak itt található: "; + +/* Discord text. [fleetcarrier.py] */ +"The scheduled carrier jump was cancelled" = "A hordozó időzített ugrása visszavonva"; + +/* Discord CMDR information. [targetmanager.py] */ +"This CMDR was added as a friend" = "Ez a CMDR barátként hozzáadva"; + +/* Text on API settings window. [api.py] */ +"This screen is used to set up a connection to a server." = "Ezen a képernyőn állítható be a szerverkapcsolat."; + +/* Preferences force tick popup text. [ui.py] */ +"This will move your current activity into the previous tick, and clear activity for the current tick." = "A jelenlegi aktivitás át lesz mozgatva az előző tickre, és a jelenlegi tick aktivitása üresre lesz állítva."; + +/* [ui.py] */ +"Tick {minutes_delta}m Overdue (Estimated)" = "A tick {minutes_delta} perc késésben (becsült)"; + +/* [ui.py] */ +"To add a webhook: Right-click on a row number and select 'Insert rows above / below'." = "Webhook hozzáadása: Jobb klikk a sor számán és válaszd a 'Sor beszúrása fölé / alá' opciót."; + +/* Discord heading. [fleetcarrier.py] */ +"To Body" = "Cél égitest:"; + +/* [ui.py] */ +"To delete a webhook: Right-click on a row number and select 'Delete rows'." = "Webhook törléséhez: Jobb klikk a sor számán és válaszd a 'Sorok törlése' opciót."; + +/* Discord heading. [fleetcarrier.py] */ +"To System" = "Cél rendszer:"; + +/* Activity window column title. [activity.py] */ +"Trade" = "Kereskedés"; + +/* Discord heading, abbreviation for trade black market profit. [default.py] */ +"TrdBMProfit" = "KerFPProfit"; + +/* Discord heading, abbreviation for trade buy. [default.py] */ +"TrdBuy" = "KerVás"; + +/* Discord heading, abbreviation for trade profit. [default.py] */ +"TrdProfit" = "KerProf"; + +/* Discord heading, abbreviation for tissue sample. [default.py] */ +"ts" = "szm"; + +/* Discord post title. [activity.py] */ +"TW Activity after Tick: {tick_time}" = "TW aktivitás {tick_time} tick után"; + +/* Dropdown menu on activity window. [activity.py] */ +"TW Only" = "Csak TW"; + +/* [ui.py] */ +"TW War Progress in {current_system}: {percent}%" = "TW állása {current_system}: {percent}%"; + +/* [sheet_options.py] */ +"Undo" = "Vissza"; + +/* Overlay CMDR information report message. [ui.py] */ +"Unknown" = "Ismeretlen"; + +/* Main window label. [ui.py] */ +"Update will be installed on shutdown" = "Leállítás után lesz telepítve a frissítés"; + +/* Discord footer message, modern embed mode. [discord.py] */ +"Updated at {date_time} (game)" = "Frissítve ekkor: {date_time} (játékidő)"; + +/* Discord message footer, legacy text mode. [discord.py] */ +"Updated at: {date_time} | {plugin_name} v{version}" = "Frissítve: {date_time} | {plugin_name} v{version}"; + +/* Discord heading, abbreviation for war INF. [default.py] */ +"WarINF" = "HáborúINF"; + +/* Preferences force tick popup text. [ui.py] */ +"WARNING: It is not usually necessary to force a tick. Only do this if you know FOR CERTAIN there has been a tick but {plugin_name} is not showing it." = "FIGYELEM: Rendszerint nem szükséges a tick kényszerítése. Csak akkor csináld, ha BIZTOSAN tudod, hogy volt tick, de a {plugin_name} nem mutatja."; + +/* Preferences checkbox label. [ui.py] */ +"Warnings" = "Figyelmeztetések"; + +/* Preferences table heading. [ui.py] */ +"Webhook URL" = "Webhook URL"; + +/* [ui.py] */ +"Within {minutes_to_tick}m of Next Tick (Estimated)" = "Következő tick {minutes_to_tick} percen belül (becsült)"; + +/* Discord heading. [default.py] */ +"wounded" = "sérült"; + +/* Label on legend window. [legend.py] */ +"Wounded evacuation missions" = "Sérült evakuáció küldetések"; + +/* [fleetcarrier.py] */ +"Yes" = "Igen"; + +/* Label on legend window. [legend.py] */ +"Zero / Low / Med / High demand level for trade buy / sell" = "Nulla / Alacsony / Közepes / Magas keresleti szint kereskedelmi vásárlás / eladáshoz"; + +/* Activity window title. [activity.py] */ +"{plugin_name} - Activity After Tick at: {tick_time}" = "{plugin_name} - Tevékenység {tick_time} tick után"; + +/* API settings window title. [api.py] */ +"{plugin_name} - API Settings" = "{plugin_name} - API beállítások"; + +/* Carrier window title. [fleetcarrier.py] */ +"{plugin_name} - Carrier {carrier_name} ({carrier_callsign}) in system: {system_name}" = "{plugin_name} - {carrier_name} ({carrier_callsign}) a(z) {system_name} rendszerben"; + +/* CMDR window title. [cmdrs.py] */ +"{plugin_name} - CMDR Interactions" = "{plugin_name} - CMDR interakciók"; + +/* Legend window title. [legend.py] */ +"{plugin_name} - Icon Legend" = "{plugin_name} - Ikon jelmagyarázat"; + +/* Preferences checkbox label. [ui.py] */ +"{plugin_name} Active" = "{plugin_name} aktív"; + +/* Overlay message. [overlay.py] */ +"{plugin_name} Ready" = "{plugin_name} kész"; + +/* Main window label. [ui.py] */ +"{plugin_name} Status:" = "{plugin_name} státusz:"; + +/* Main window error message. [tick.py] */ +"{plugin_name} WARNING: Unable to fetch latest tick" = "{plugin_name} FIGYELMEZTETÉS: Nem kérhető le a legutolsó tick"; + +/* Main window error message. [updatemanager.py] */ +"{plugin_name}: There was a problem saving the new version" = "{plugin_name}: Hiba történt az új verzió mentése során"; + +/* Main window error message. [updatemanager.py] */ +"{plugin_name}: Unable to fetch latest plugin download" = "{plugin_name}: Nem kérhető le a legutolsó plugin letöltés"; + +/* Main window error message. [updatemanager.py] */ +"{plugin_name}: Unable to fetch latest plugin version" = "{plugin_name}: Nem kérhető le a legutolsó plugin verzió"; From 061b6d313f611471faa481fc754c4c51c859d368 Mon Sep 17 00:00:00 2001 From: aussig Date: Sun, 22 Dec 2024 18:20:07 +0000 Subject: [PATCH 27/27] Update CHANGELOG and version for release. --- CHANGELOG.md | 2 +- load.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b36108..9456552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## vx.x.x - xxxx-xx-xx +## v4.2.0 - 2024-12-22 ### New Features: diff --git a/load.py b/load.py index e20b900..442f908 100644 --- a/load.py +++ b/load.py @@ -9,7 +9,7 @@ from bgstally.debug import Debug PLUGIN_NAME = "BGS-Tally" -PLUGIN_VERSION = semantic_version.Version.coerce("4.2.0-dev") +PLUGIN_VERSION = semantic_version.Version.coerce("4.2.0") # Initialise the main plugin class bgstally.globals.this = this = BGSTally(PLUGIN_NAME, PLUGIN_VERSION)