From bba376b60fb7ac660a4cd2d91975578ec409cb23 Mon Sep 17 00:00:00 2001 From: Shinga13 <93780215+Shinga13@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:27:47 +0100 Subject: [PATCH] shut up flake8 --- OSCRUI/app.py | 262 ++++++++++++++++++++++---------------- OSCRUI/datafunctions.py | 87 +++++++------ OSCRUI/datamodels.py | 71 ++++++----- OSCRUI/displayer.py | 44 ++++--- OSCRUI/iofunctions.py | 29 +++-- OSCRUI/leagueconnector.py | 8 +- OSCRUI/style.py | 50 +++++--- OSCRUI/textedit.py | 30 +++-- OSCRUI/widgetbuilder.py | 131 +++++++++++-------- OSCRUI/widgets.py | 44 ++++--- main.py | 73 ++++++----- 11 files changed, 491 insertions(+), 338 deletions(-) diff --git a/OSCRUI/app.py b/OSCRUI/app.py index 423f6cf..3524664 100644 --- a/OSCRUI/app.py +++ b/OSCRUI/app.py @@ -1,7 +1,8 @@ -from signal import signal, SIGINT, SIG_DFL + import os -from PySide6.QtWidgets import QApplication, QWidget, QLineEdit, QFrame, QListWidget, QTabWidget, QTableView +from PySide6.QtWidgets import QApplication, QWidget, QLineEdit, QFrame, QListWidget, QTabWidget +from PySide6. QtWidgets import QTableView from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QGridLayout from PySide6.QtCore import QSize, QSettings from PySide6.QtGui import QIntValidator @@ -11,17 +12,19 @@ from .iofunctions import load_icon_series, get_asset_path, load_icon from .textedit import format_path from .widgets import BannerLabel, FlipButton, WidgetStorage, AnalysisPlot -from .widgetbuilder import SMAXMAX, SMAXMIN, SMINMAX, SMINMIN, ALEFT, ARIGHT, ATOP, ACENTER, AHCENTER, ABOTTOM +from .widgetbuilder import SMAXMAX, SMAXMIN, SMINMAX, SMINMIN, ALEFT, ARIGHT, ATOP, ACENTER +from .widgetbuilder import AHCENTER, ABOTTOM from .leagueconnector import OSCRClient # only for developing; allows to terminate the qt event loop with keyboard interrupt +from signal import signal, SIGINT, SIG_DFL signal(SIGINT, SIG_DFL) -class OSCRUI(): +class OSCRUI(): - from .datafunctions import init_parser, copy_summary_callback, copy_analysis_callback - from .datafunctions import analyze_log_callback, update_shown_columns_dmg, update_shown_columns_heal + from .datafunctions import analyze_log_callback, copy_analysis_callback, copy_summary_callback + from .datafunctions import init_parser, update_shown_columns_dmg, update_shown_columns_heal from .displayer import create_legend_item from .iofunctions import browse_path from .style import get_style_class, create_style_sheet, theme_font, get_style @@ -32,9 +35,9 @@ class OSCRUI(): app_dir = None - config = {} # see main.py for contents + config = {} # see main.py for contents - settings: QSettings # see main.py for defaults + settings: QSettings # see main.py for defaults # stores widgets that need to be accessed from outside their creating function widgets: WidgetStorage @@ -74,7 +77,7 @@ def run(self) -> int: :return: exit code of event loop """ return self.app.exec() - + def cache_assets(self): """ Caches assets like icon images @@ -108,7 +111,7 @@ def init_settings(self): self.settings.setValue(setting, value) if not self.settings.value('log_path', ''): self.settings.setValue('log_path', format_path(self.app_dir)) - + def init_config(self): """ Prepares config. @@ -117,14 +120,15 @@ def init_config(self): self.current_combat_path = '' self.config['templog_folder_path'] = os.path.abspath( self.app_dir + self.config['templog_folder_path']) - + @property def parser_settings(self) -> dict: """ Returns settings relevant to the parser """ - relevant_settings = (('combats_to_parse', int), ('seconds_between_combats', int), - ('excluded_event_ids', list), ('graph_resolution', float)) + relevant_settings = ( + ('combats_to_parse', int), ('seconds_between_combats', int), + ('excluded_event_ids', list), ('graph_resolution', float)) settings = dict() for setting_key, settings_type in relevant_settings: setting = self.settings.value(setting_key, type=settings_type, defaultValue='') @@ -133,7 +137,6 @@ def parser_settings(self) -> dict: settings['templog_folder_path'] = self.config['templog_folder_path'] return settings - @property def sidebar_item_width(self) -> int: """ @@ -157,7 +160,7 @@ def main_window_resize_callback(self, event): self.current_combats.setFixedWidth(self.sidebar_item_width) self.widgets.ladder_map.setFixedWidth(self.sidebar_item_width) event.accept() - + # ------------------------------------------------------------------------------------------------------- # GUI functions below # ------------------------------------------------------------------------------------------------------- @@ -177,7 +180,7 @@ def create_main_window(self, argv=[]) -> tuple[QApplication, QWidget]: int(self.config['minimum_window_width']), int(self.config['minimum_window_height'])) if self.settings.value('geometry'): - window.restoreGeometry(self.settings.value('geometry')) + window.restoreGeometry(self.settings.value('geometry')) window.closeEvent = self.main_window_close_callback window.resizeEvent = self.main_window_resize_callback return app, window @@ -197,8 +200,9 @@ def setup_main_layout(self): left.setSizePolicy(SMAXMIN) content_layout.addWidget(left, 0, 0) - right = self.create_frame(main_frame, 'frame', - {'border-left-style': 'solid', 'border-left-width': '@sep', 'border-left-color': '@oscr' }) + right = self.create_frame(main_frame, 'frame', style_override={ + 'border-left-style': 'solid', 'border-left-width': '@sep', + 'border-left-color': '@oscr'}) right.setSizePolicy(SMINMIN) right.hide() content_layout.addWidget(right, 0, 4) @@ -246,8 +250,9 @@ def setup_left_sidebar_league(self): map_label = self.create_label('Available Maps:', 'label_heading', frame) left_layout.addWidget(map_label) - background_frame = self.create_frame(frame, 'light_frame', - {'border-radius': self.theme['listbox']['border-radius'], 'margin-top': '@csp'}, SMAXMIN) + background_frame = self.create_frame(frame, 'light_frame', style_override={ + 'border-radius': self.theme['listbox']['border-radius'], 'margin-top': '@csp'}, + size_policy=SMAXMIN) background_layout = QVBoxLayout() background_layout.setContentsMargins(0, 0, 0, 0) background_frame.setLayout(background_layout) @@ -263,7 +268,6 @@ def setup_left_sidebar_league(self): frame.setLayout(left_layout) - def setup_left_sidebar_log(self): """ Sets up the log management tab of the left sidebar @@ -278,7 +282,8 @@ def setup_left_sidebar_log(self): head_layout = QHBoxLayout() head = self.create_label('STO Combatlog:', 'label', frame) head_layout.addWidget(head, alignment=ALEFT) - cut_log_button = self.create_icon_button(self.icons['log-cut'], 'Manage Logfile', parent=frame) + cut_log_button = self.create_icon_button( + self.icons['log-cut'], 'Manage Logfile', parent=frame) cut_log_button.clicked.connect(self.split_dialog) head_layout.addWidget(cut_log_button, alignment=ARIGHT) left_layout.addLayout(head_layout) @@ -288,12 +293,12 @@ def setup_left_sidebar_log(self): self.entry.setFont(self.theme_font('entry')) self.entry.setFixedWidth(self.sidebar_item_width) left_layout.addWidget(self.entry) - + entry_button_config = { 'default': {'margin-bottom': '@isp'}, 'Browse ...': {'callback': lambda: self.browse_log(self.entry), 'align': ALEFT}, - 'Scan': {'callback': lambda: self.analyze_log_callback(path=self.entry.text(), parser_num=1), - 'align': ARIGHT} + 'Scan': {'callback': lambda: self.analyze_log_callback( + path=self.entry.text(), parser_num=1), 'align': ARIGHT} } entry_buttons = self.create_button_series(frame, entry_button_config, 'button') left_layout.addLayout(entry_buttons) @@ -306,9 +311,11 @@ def setup_left_sidebar_log(self): combat_button_layout.setContentsMargins(0, 0, 0, 0) combat_button_layout.setSpacing(m) combat_button_layout.setAlignment(ALEFT) - export_button = self.create_icon_button(self.icons['export-parse'], 'Export Combat', parent=frame) + export_button = self.create_icon_button( + self.icons['export-parse'], 'Export Combat', parent=frame) combat_button_layout.addWidget(export_button) - save_button = self.create_icon_button(self.icons['save'], 'Save Combat to Cache', parent=frame) + save_button = self.create_icon_button( + self.icons['save'], 'Save Combat to Cache', parent=frame) combat_button_layout.addWidget(save_button) top_button_row.addLayout(combat_button_layout) @@ -316,11 +323,13 @@ def setup_left_sidebar_log(self): navigation_button_layout.setContentsMargins(0, 0, 0, 0) navigation_button_layout.setSpacing(m) navigation_button_layout.setAlignment(AHCENTER) - up_button = self.create_icon_button(self.icons['log-up'], 'Load newer Combats', parent=frame) + up_button = self.create_icon_button( + self.icons['log-up'], 'Load newer Combats', parent=frame) up_button.setEnabled(False) navigation_button_layout.addWidget(up_button) self.widgets.navigate_up_button = up_button - down_button = self.create_icon_button(self.icons['log-down'], 'Load older Combats', parent=frame) + down_button = self.create_icon_button( + self.icons['log-down'], 'Load older Combats', parent=frame) down_button.setEnabled(False) navigation_button_layout.addWidget(down_button) self.widgets.navigate_down_button = down_button @@ -330,16 +339,19 @@ def setup_left_sidebar_log(self): parser_button_layout.setContentsMargins(0, 0, 0, 0) parser_button_layout.setSpacing(m) parser_button_layout.setAlignment(ARIGHT) - parser1_button = self.create_icon_button(self.icons['parser-left'], 'Analyze Combat', parent=frame) + parser1_button = self.create_icon_button( + self.icons['parser-left'], 'Analyze Combat', parent=frame) parser_button_layout.addWidget(parser1_button) - parser2_button = self.create_icon_button(self.icons['parser-right'], 'Analyze Combat', parent=frame) + parser2_button = self.create_icon_button( + self.icons['parser-right'], 'Analyze Combat', parent=frame) parser_button_layout.addWidget(parser2_button) top_button_row.addLayout(parser_button_layout) left_layout.addLayout(top_button_row) - background_frame = self.create_frame(frame, 'light_frame', - {'border-radius': self.theme['listbox']['border-radius'], 'margin-top': '@csp'}, SMAXMIN) + background_frame = self.create_frame(frame, 'light_frame', style_override={ + 'border-radius': self.theme['listbox']['border-radius'], 'margin-top': '@csp'}, + size_policy=SMAXMIN) background_layout = QVBoxLayout() background_layout.setContentsMargins(0, 0, 0, 0) background_frame.setLayout(background_layout) @@ -350,7 +362,7 @@ def setup_left_sidebar_log(self): self.current_combats.setFixedWidth(self.sidebar_item_width) background_layout.addWidget(self.current_combats) left_layout.addWidget(background_frame, stretch=1) - + parser1_button.clicked.connect( lambda: self.analyze_log_callback(self.current_combats.currentRow(), parser_num=1)) export_button.clicked.connect(lambda: self.save_combat(self.current_combats.currentRow())) @@ -373,7 +385,7 @@ def setup_left_sidebar_tabber(self, frame: QFrame): league_frame = self.create_frame(style='medium_frame') sidebar_tabber = QTabWidget(frame) sidebar_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber')) - sidebar_tabber.tabBar().setStyleSheet(self.get_style_class( 'QTabBar', 'tabber_tab')) + sidebar_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab')) sidebar_tabber.setSizePolicy(SMAXMIN) sidebar_tabber.addTab(log_frame, 'Log') sidebar_tabber.addTab(league_frame, 'League') @@ -389,7 +401,7 @@ def setup_left_sidebar_tabber(self, frame: QFrame): self.setup_left_sidebar_log() self.setup_left_sidebar_league() - def setup_main_tabber(self, frame:QFrame): + def setup_main_tabber(self, frame: QFrame): """ Sets up the tabber switching between Overview, Analysis, League and Settings. @@ -403,7 +415,7 @@ def setup_main_tabber(self, frame:QFrame): main_tabber = QTabWidget(frame) main_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber')) - main_tabber.tabBar().setStyleSheet(self.get_style_class( 'QTabBar', 'tabber_tab')) + main_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab')) main_tabber.setSizePolicy(SMINMIN) main_tabber.addTab(o_frame, '&O') main_tabber.addTab(a_frame, '&A') @@ -418,7 +430,8 @@ def setup_main_tabber(self, frame:QFrame): self.widgets.main_menu_buttons[0].clicked.connect(lambda: self.switch_main_tab(0)) self.widgets.main_menu_buttons[1].clicked.connect(lambda: self.switch_main_tab(1)) self.widgets.main_menu_buttons[2].clicked.connect(lambda: self.switch_main_tab(2)) - self.widgets.main_menu_buttons[2].clicked.connect(lambda: self.establish_league_connection(True)) + self.widgets.main_menu_buttons[2].clicked.connect( + lambda: self.establish_league_connection(True)) self.widgets.main_menu_buttons[3].clicked.connect(lambda: self.switch_main_tab(3)) self.widgets.main_tab_frames.append(o_frame) self.widgets.main_tab_frames.append(a_frame) @@ -459,12 +472,15 @@ def setup_overview_frame(self): switch_style = { 'default': {'margin-left': '@margin', 'margin-right': '@margin'}, - 'DPS Bar': {'callback': lambda state: self.switch_overview_tab(0), 'align': ACENTER, 'toggle': True}, - 'DPS Graph': {'callback': lambda state: self.switch_overview_tab(1), 'align': ACENTER, 'toggle': False}, - 'Damage Graph': {'callback': lambda state: self.switch_overview_tab(2), 'align': ACENTER, - 'toggle': False} + 'DPS Bar': { + 'callback': lambda: self.switch_overview_tab(0), 'align': ACENTER, 'toggle': True}, + 'DPS Graph': { + 'callback': lambda: self.switch_overview_tab(1), 'align': ACENTER, 'toggle': False}, + 'Damage Graph': { + 'callback': lambda: self.switch_overview_tab(2), 'align': ACENTER, 'toggle': False} } - switcher, buttons = self.create_button_series(switch_frame, switch_style, 'tab_button', ret=True) + switcher, buttons = self.create_button_series( + switch_frame, switch_style, 'tab_button', ret=True) switcher.setContentsMargins(0, self.theme['defaults']['margin'], 0, 0) switch_frame.setLayout(switcher) self.widgets.overview_menu_buttons = buttons @@ -477,7 +493,7 @@ def setup_overview_frame(self): ladder_button = self.create_icon_button(self.icons['ladder'], 'Upload Result') ladder_button.clicked.connect(self.upload_callback) icon_layout.addWidget(ladder_button) - switch_layout.addLayout(icon_layout, 0, 2, alignment = ARIGHT | ABOTTOM) + switch_layout.addLayout(icon_layout, 0, 2, alignment=ARIGHT | ABOTTOM) switch_layout.setColumnStretch(2, 1) o_frame.setLayout(layout) self.widgets.overview_tabber = o_tabber @@ -498,7 +514,7 @@ def setup_analysis_frame(self): switch_layout = QGridLayout() switch_layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(switch_layout) - + a_tabber = QTabWidget(a_frame) a_tabber.setStyleSheet(self.get_style_class('QTabWidget', 'tabber')) a_tabber.tabBar().setStyleSheet(self.get_style_class('QTabBar', 'tabber_tab')) @@ -516,16 +532,21 @@ def setup_analysis_frame(self): switch_style = { 'default': {'margin-left': '@margin', 'margin-right': '@margin'}, - 'Damage Out': {'callback': lambda state: self.switch_analysis_tab(0), 'align': ACENTER, - 'toggle': True}, - 'Damage Taken': {'callback': lambda state: self.switch_analysis_tab(1), 'align': ACENTER, - 'toggle': False}, - 'Heals Out': {'callback': lambda state: self.switch_analysis_tab(2), 'align': ACENTER, - 'toggle': False}, - 'Heals In': {'callback': lambda state: self.switch_analysis_tab(3), 'align': ACENTER, - 'toggle': False} + 'Damage Out': { + 'callback': lambda state: self.switch_analysis_tab(0), 'align': ACENTER, + 'toggle': True}, + 'Damage Taken': { + 'callback': lambda state: self.switch_analysis_tab(1), + 'align': ACENTER, 'toggle': False}, + 'Heals Out': { + 'callback': lambda state: self.switch_analysis_tab(2), 'align': ACENTER, + 'toggle': False}, + 'Heals In': { + 'callback': lambda state: self.switch_analysis_tab(3), 'align': ACENTER, + 'toggle': False} } - switcher, buttons = self.create_button_series(switch_frame, switch_style, 'tab_button', ret=True) + switcher, buttons = self.create_button_series( + switch_frame, switch_style, 'tab_button', ret=True) switcher.setContentsMargins(0, self.theme['defaults']['margin'], 0, 0) switch_frame.setLayout(switcher) self.widgets.analysis_menu_buttons = buttons @@ -539,11 +560,11 @@ def setup_analysis_frame(self): copy_button = self.create_icon_button(self.icons['copy'], 'Copy Data') copy_button.clicked.connect(self.copy_analysis_callback) copy_layout.addWidget(copy_button) - switch_layout.addLayout(copy_layout, 0, 2, alignment = ARIGHT | ABOTTOM) + switch_layout.addLayout(copy_layout, 0, 2, alignment=ARIGHT | ABOTTOM) switch_layout.setColumnStretch(2, 1) tabs = ( - (dout_frame, 'analysis_table_dout', 'analysis_plot_dout'), + (dout_frame, 'analysis_table_dout', 'analysis_plot_dout'), (dtaken_frame, 'analysis_table_dtaken', 'analysis_plot_dtaken'), (hout_frame, 'analysis_table_hout', 'analysis_plot_hout'), (hin_frame, 'analysis_table_hin', 'analysis_plot_hin') @@ -558,7 +579,7 @@ def setup_analysis_frame(self): plot_layout = QHBoxLayout() plot_layout.setContentsMargins(0, 0, 0, 0) plot_layout.setSpacing(self.theme['defaults']['isp']) - + plot_bundle_frame = self.create_frame(plot_frame, size_policy=SMINMAX) plot_bundle_layout = QVBoxLayout() plot_bundle_layout.setContentsMargins(0, 0, 0, 0) @@ -568,7 +589,8 @@ def setup_analysis_frame(self): plot_legend_layout.setContentsMargins(0, 0, 0, 0) plot_legend_layout.setSpacing(2 * self.theme['defaults']['margin']) plot_legend_frame.setLayout(plot_legend_layout) - plot_widget = AnalysisPlot(self.theme['plot']['color_cycler'], self.theme['defaults']['fg'], + plot_widget = AnalysisPlot( + self.theme['plot']['color_cycler'], self.theme['defaults']['fg'], self.theme_font('plot_widget'), plot_legend_layout) setattr(self.widgets, plot_name, plot_widget) plot_widget.setStyleSheet(self.get_style('plot_widget_nullifier')) @@ -582,7 +604,8 @@ def setup_analysis_frame(self): plot_button_layout = QVBoxLayout() plot_button_layout.setContentsMargins(0, 0, 0, 0) plot_button_layout.setSpacing(0) - freeze_button = self.create_button('Freeze Graph', 'toggle_button', plot_button_frame, + freeze_button = self.create_button( + 'Freeze Graph', 'toggle_button', plot_button_frame, style_override={'border-color': '@bg'}, toggle=True) freeze_button.clicked.connect(plot_widget.toggle_freeze) plot_button_layout.addWidget(freeze_button, alignment=ARIGHT) @@ -592,15 +615,15 @@ def setup_analysis_frame(self): plot_button_frame.setLayout(plot_button_layout) plot_layout.addWidget(plot_button_frame, stretch=0) - plot_frame.setLayout(plot_layout) + plot_frame.setLayout(plot_layout) tab_layout.addWidget(plot_frame, stretch=3) tree = self.create_analysis_table(tab, 'tree_table') setattr(self.widgets, table_name, tree) tree.clicked.connect(lambda index, pw=plot_widget: self.slot_analysis_graph(index, pw)) tab_layout.addWidget(tree, stretch=7) - tab.setLayout(tab_layout) - + tab.setLayout(tab_layout) + a_frame.setLayout(layout) def slot_analysis_graph(self, index, plot_widget: AnalysisPlot): @@ -620,21 +643,13 @@ def setup_league_standings_frame(self): """ l_frame = self.widgets.main_tab_frames[2] layout = QVBoxLayout() - margin = self.theme['defaults']['margin'] layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) - # spacing = self.theme['defaults']['isp'] - # control_frame = self.create_frame(l_frame) - # control_frame_layout = QHBoxLayout() - # control_frame_layout.setContentsMargins(margin, margin, margin, margin) - # control_frame_layout.setSpacing(spacing) - ladder_table = QTableView(l_frame) self.style_table(ladder_table, {'margin': '@margin'}) self.widgets.ladder_table = ladder_table - # layout.addWidget(control_frame, alignment=AHCENTER) layout.addWidget(ladder_table) l_frame.setLayout(layout) @@ -647,7 +662,7 @@ def create_master_layout(self, parent) -> tuple[QVBoxLayout, QFrame]: :return: populated QVBoxlayout and content frame QFrame """ - layout = QVBoxLayout() + layout = QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) bg_frame = self.create_frame(parent, 'frame', {'background': '@oscr'}) bg_frame.setSizePolicy(SMINMIN) @@ -660,23 +675,23 @@ def create_master_layout(self, parent) -> tuple[QVBoxLayout, QFrame]: main_layout.addWidget(lbl) - menu_frame = self.create_frame(bg_frame, 'frame', {'background':'@oscr'}) + menu_frame = self.create_frame(bg_frame, 'frame', {'background': '@oscr'}) menu_frame.setSizePolicy(SMAXMAX) menu_frame.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(menu_frame) menu_button_style = { - 'Overview': {'style':{'margin-left': '@isp'}}, + 'Overview': {'style': {'margin-left': '@isp'}}, 'Analysis': {}, 'League Standings': {}, 'Settings': {}, } - bt_lay, buttons = self.create_button_series(menu_frame, menu_button_style, - style='menu_button', seperator='•', ret=True) + bt_lay, buttons = self.create_button_series( + menu_frame, menu_button_style, style='menu_button', seperator='•', ret=True) menu_frame.setLayout(bt_lay) self.widgets.main_menu_buttons = buttons w = self.theme['app']['frame_thickness'] - main_frame = self.create_frame(bg_frame, 'frame', {'margin':(0, w, w, w)}) + main_frame = self.create_frame(bg_frame, 'frame', {'margin': (0, w, w, w)}) main_frame.setSizePolicy(SMINMIN) main_layout.addWidget(main_frame) bg_frame.setLayout(main_layout) @@ -704,6 +719,10 @@ def setup_settings_frame(self): settings_layout.addWidget(col_3_frame, alignment=ATOP, stretch=1) # first column + hider_frame_style_override = { + 'border-color': '@lbg', + 'border-width': '@bw', 'border-style': 'solid', 'border-radius': 2 + } col_1 = QHBoxLayout() col_1.setContentsMargins(0, 0, 0, 0) col_1.setSpacing(self.theme['defaults']['isp']) @@ -712,22 +731,25 @@ def setup_settings_frame(self): dmg_hider_label = self.create_label('Damage table columns:', 'label_subhead') col_1_1.addWidget(dmg_hider_label) dmg_hider_layout = QVBoxLayout() - dmg_hider_frame = self.create_frame(col_1_frame, size_policy=SMINMAX, style_override= - {'border-color':'@lbg', 'border-width':'@bw', 'border-style':'solid', 'border-radius': 2}) + dmg_hider_frame = self.create_frame( + col_1_frame, size_policy=SMINMAX, style_override=hider_frame_style_override) self.set_buttons = list() for i, head in enumerate(TREE_HEADER[1:]): - bt = self.create_button(head, 'toggle_button', dmg_hider_frame, + bt = self.create_button( + head, 'toggle_button', dmg_hider_frame, toggle=self.settings.value(f'dmg_columns|{i}', type=bool)) bt.setSizePolicy(SMINMAX) - bt.clicked[bool].connect(lambda state, i=i: self.settings.setValue(f'dmg_columns|{i}', state)) + bt.clicked[bool].connect( + lambda state, i=i: self.settings.setValue(f'dmg_columns|{i}', state)) dmg_hider_layout.addWidget(bt, stretch=1) - dmg_seperator = self.create_frame(dmg_hider_frame, 'hr', style_override={'background-color': '@lbg'}, + dmg_seperator = self.create_frame( + dmg_hider_frame, 'hr', style_override={'background-color': '@lbg'}, size_policy=SMINMIN) dmg_seperator.setFixedHeight(self.theme['defaults']['bw']) dmg_hider_layout.addWidget(dmg_seperator) apply_button = self.create_button('Apply', 'button', dmg_hider_frame) apply_button.clicked.connect(self.update_shown_columns_dmg) - dmg_hider_layout.addWidget(apply_button, alignment=ARIGHT|ATOP) + dmg_hider_layout.addWidget(apply_button, alignment=ARIGHT | ATOP) dmg_hider_frame.setLayout(dmg_hider_layout) col_1_1.addWidget(dmg_hider_frame, alignment=ATOP) col_1.addLayout(col_1_1, stretch=1) @@ -737,21 +759,23 @@ def setup_settings_frame(self): heal_hider_label = self.create_label('Heal table columns:', 'label_subhead', col_1_frame) col_1_2.addWidget(heal_hider_label) heal_hider_layout = QVBoxLayout() - heal_hider_frame = self.create_frame(col_1_frame, size_policy=SMINMAX, style_override= - {'border-color':'@lbg', 'border-width':'@bw', 'border-style':'solid', 'border-radius': 2}) + heal_hider_frame = self.create_frame( + col_1_frame, size_policy=SMINMAX, style_override=hider_frame_style_override) for i, head in enumerate(HEAL_TREE_HEADER[1:]): - bt = self.create_button(head, 'toggle_button', heal_hider_frame, + bt = self.create_button( + head, 'toggle_button', heal_hider_frame, toggle=self.settings.value(f'heal_columns|{i}', type=bool)) bt.setSizePolicy(SMINMAX) - bt.clicked[bool].connect(lambda state, i=i: self.settings.setValue(f'heal_columns|{i}', state)) + bt.clicked[bool].connect( + lambda state, i=i: self.settings.setValue(f'heal_columns|{i}', state)) heal_hider_layout.addWidget(bt, stretch=1) - heal_seperator = self.create_frame(dmg_hider_frame, 'hr', style_override={'background-color': '@lbg'}, - size_policy=SMINMIN) + heal_seperator = self.create_frame( + dmg_hider_frame, 'hr', style_override={'background-color': '@lbg'}, size_policy=SMINMIN) heal_seperator.setFixedHeight(self.theme['defaults']['bw']) heal_hider_layout.addWidget(heal_seperator) apply_button_2 = self.create_button('Apply', 'button', heal_hider_frame) apply_button_2.clicked.connect(self.update_shown_columns_heal) - heal_hider_layout.addWidget(apply_button_2, alignment=ARIGHT|ATOP) + heal_hider_layout.addWidget(apply_button_2, alignment=ARIGHT | ATOP) heal_hider_frame.setLayout(heal_hider_layout) col_1_2.addWidget(heal_hider_frame, alignment=ATOP) col_1.addLayout(col_1_2, stretch=1) @@ -767,45 +791,49 @@ def setup_settings_frame(self): col_2.addWidget(combat_delta_label, 0, 0, alignment=ARIGHT) combat_delta_validator = QIntValidator() combat_delta_validator.setBottom(1) - combat_delta_entry = self.create_entry(self.settings.value('seconds_between_combats', type=str), + combat_delta_entry = self.create_entry( + self.settings.value('seconds_between_combats', type=str), combat_delta_validator, style_override={'margin-top': 0}) - combat_delta_entry.editingFinished.connect(lambda: self.settings.setValue('seconds_between_combats', - combat_delta_entry.text())) + combat_delta_entry.editingFinished.connect(lambda: self.settings.setValue( + 'seconds_between_combats', combat_delta_entry.text())) col_2.addWidget(combat_delta_entry, 0, 1, alignment=ALEFT) combat_num_label = self.create_label('Number of combats to isolate:', 'label_subhead') col_2.addWidget(combat_num_label, 1, 0, alignment=ARIGHT) combat_num_validator = QIntValidator() combat_num_validator.setBottom(1) - combat_num_entry = self.create_entry(self.settings.value('combats_to_parse', type=str), - combat_num_validator, style_override={'margin-top': 0}) - combat_num_entry.editingFinished.connect(lambda: self.settings.setValue('combats_to_parse', - combat_num_entry.text())) + combat_num_entry = self.create_entry( + self.settings.value('combats_to_parse', type=str), combat_num_validator, + style_override={'margin-top': 0}) + combat_num_entry.editingFinished.connect(lambda: self.settings.setValue( + 'combats_to_parse', combat_num_entry.text())) col_2.addWidget(combat_num_entry, 1, 1, alignment=ALEFT) - graph_resolution_label = self.create_label('Graph resolution (data points per second):', - 'label_subhead') + graph_resolution_label = self.create_label( + 'Graph resolution (data points per second):', 'label_subhead') col_2.addWidget(graph_resolution_label, 2, 0, alignment=ARIGHT) graph_resolution_validator = QIntValidator(1, 5) - default_graph_resolution = str(int(round(1 / self.settings.value('graph_resolution', type=float), 0))) - graph_resolution_entry = self.create_entry(default_graph_resolution, graph_resolution_validator, + default_graph_resolution = f"{1 / self.settings.value('graph_resolution', type=float):.0f}" + graph_resolution_entry = self.create_entry( + default_graph_resolution, graph_resolution_validator, style_override={'margin-top': 0}) - graph_resolution_entry.editingFinished.connect(lambda: self.settings.setValue('graph_resolution', - round(1 / int(graph_resolution_entry.text()), 1))) + graph_resolution_entry.editingFinished.connect( + lambda: self.set_graph_resolution_setting(graph_resolution_entry.text())) col_2.addWidget(graph_resolution_entry, 2, 1, alignment=ALEFT) split_length_label = self.create_label('Auto Split After Lines:', 'label_subhead') col_2.addWidget(split_length_label, 3, 0, alignment=ARIGHT) split_length_validator = QIntValidator() split_length_validator.setBottom(1) - split_length_entry = self.create_entry(self.settings.value('split_log_after', type=str), - split_length_validator, style_override={'margin-top': 0}) - split_length_entry.editingFinished.connect(lambda: self.settings.setValue('split_log_after', - split_length_entry.text())) + split_length_entry = self.create_entry( + self.settings.value('split_log_after', type=str), split_length_validator, + style_override={'margin-top': 0}) + split_length_entry.editingFinished.connect(lambda: self.settings.setValue( + 'split_log_after', split_length_entry.text())) col_2.addWidget(split_length_entry, 3, 1, alignment=ALEFT) - + col_2_frame.setLayout(col_2) settings_frame.setLayout(settings_layout) - def browse_log(self, entry:QLineEdit): + def browse_log(self, entry: QLineEdit): """ Callback for browse button. @@ -814,7 +842,8 @@ def browse_log(self, entry:QLineEdit): """ current_path = entry.text() if current_path != '': - path = self.browse_path(os.path.dirname(current_path), 'Logfile (*.log);;Any File (*.*)') + path = self.browse_path( + os.path.dirname(current_path), 'Logfile (*.log);;Any File (*.*)') if path != '': entry.setText(format_path(path)) @@ -867,7 +896,7 @@ def switch_analysis_tab(self, tab_index: int): button.setChecked(False) else: button.setChecked(True) - + def switch_overview_tab(self, tab_index: int): """ Callback for tab switch buttons; switches tab and sets active button. @@ -903,3 +932,16 @@ def set_variable(self, var_to_be_set, index, value): Assigns value at index to variable """ var_to_be_set[index] = value + + def set_graph_resolution_setting(self, setting_text: str): + """ + Calculates inverse of setting_text and stores it to settings. + + Parameters: + - :param setting_text: data points per second + """ + try: + value = round(1 / int(setting_text), 1) + self.settings.setValue('graph_resolution', value) + except (ValueError, ZeroDivisionError): + return diff --git a/OSCRUI/datafunctions.py b/OSCRUI/datafunctions.py index c59c806..55843cb 100644 --- a/OSCRUI/datafunctions.py +++ b/OSCRUI/datafunctions.py @@ -1,12 +1,12 @@ import os -from PySide6.QtCore import QThread, Signal, Qt -from OSCR import OSCR, TREE_HEADER, HEAL_TREE_HEADER +from OSCR import OSCR, HEAL_TREE_HEADER, TREE_HEADER +from PySide6.QtCore import Qt, QThread, Signal from .datamodels import DamageTreeModel, HealTreeModel, TreeSelectionModel from .displayer import create_overview -from .widgetbuilder import show_warning, log_size_warning, split_dialog from .textedit import format_damage_tree_data, format_heal_tree_data +from .widgetbuilder import log_size_warning, show_warning, split_dialog class CustomThread(QThread): @@ -23,6 +23,7 @@ def run(self): r = self._func() self.result.emit((r,)) + def init_parser(self): """ Initializes Parser. @@ -30,12 +31,14 @@ def init_parser(self): self.parser1 = OSCR(settings=self.parser_settings) # self.parser2 = OSCR() + def analyze_log_callback(self, combat_id=None, path=None, parser_num: int = 1): """ Wrapper function for retrieving and showing data. Callback of "Analyse" and "Refresh" button. - + Parameters: - - :param combat_id: id of older combat (0 -> latest combat in the file; len(...) - 1 -> oldest combat) + - :param combat_id: id of older combat (0 -> latest combat in the file; + len(...) - 1 -> oldest combat) - :param path: path to combat log file """ if combat_id == -1 or combat_id == self.current_combat_id: @@ -46,11 +49,12 @@ def analyze_log_callback(self, combat_id=None, path=None, parser_num: int = 1): parser: OSCR = self.parser2 else: return - + # initial run / click on the Analyze buttonQGuiApplication if combat_id is None: if not path or not os.path.isfile(path): - show_warning(self, 'Invalid Logfile', 'The Logfile you are trying to open does not exist.') + show_warning( + self, 'Invalid Logfile', 'The Logfile you are trying to open does not exist.') return if path != self.settings.value('log_path'): self.settings.setValue('log_path', path) @@ -84,6 +88,7 @@ def analyze_log_callback(self, combat_id=None, path=None, parser_num: int = 1): self.widgets.main_tabber.setCurrentIndex(0) self.widgets.overview_tabber.setCurrentIndex(0) + def copy_summary_callback(self): """ Callback to set the combat summary of the active combat to the user's clippboard @@ -112,9 +117,9 @@ def copy_summary_callback(self): def get_data(self, combat: int | None = None, path: str | None = None): """ - Interface between OSCRUI and OSCR. + Interface between OSCRUI and OSCR. Uses OSCR class to analyze log at path - + :return: False to abort analyzing process, True otherwise """ @@ -133,12 +138,13 @@ def get_data(self, combat: int | None = None, path: str | None = None): else: return False self.parser1.shallow_combat_analysis(0) - + # same log file, old combat else: self.parser1.shallow_combat_analysis(combat) return True + def analysis_data_slot(self, item_tuple: tuple): """ Inserts the data retrieved from the parser into the respective tables @@ -149,6 +155,7 @@ def analysis_data_slot(self, item_tuple: tuple): populate_analysis(self, *item_tuple) self.widgets.main_menu_buttons[1].setDisabled(False) + def populate_analysis(self, root_items: tuple): """ Populates the Analysis' treeview table. @@ -156,48 +163,49 @@ def populate_analysis(self, root_items: tuple): damage_out_item, damage_in_item, heal_out_item, heal_in_item = root_items damage_out_table = self.widgets.analysis_table_dout - damage_out_model = DamageTreeModel(damage_out_item, self.theme_font('tree_table_header'), - self.theme_font('tree_table'), + damage_out_model = DamageTreeModel( + damage_out_item, self.theme_font('tree_table_header'), self.theme_font('tree_table'), self.theme_font('', self.theme['tree_table']['::item']['font'])) damage_out_table.setModel(damage_out_model) - damage_out_table.expand(damage_out_model.index(0, 0, - damage_out_model.createIndex(0, 0, damage_out_model._root))) + damage_out_root_index = damage_out_model.createIndex(0, 0, damage_out_model._root) + damage_out_table.expand(damage_out_model.index(0, 0, damage_out_root_index)) damage_out_table.sortByColumn(1, Qt.SortOrder.AscendingOrder) damage_out_table.setSelectionModel(TreeSelectionModel(damage_out_model)) damage_in_table = self.widgets.analysis_table_dtaken - damage_in_model = DamageTreeModel(damage_in_item, self.theme_font('tree_table_header'), - self.theme_font('tree_table'), + damage_in_model = DamageTreeModel( + damage_in_item, self.theme_font('tree_table_header'), self.theme_font('tree_table'), self.theme_font('', self.theme['tree_table']['::item']['font'])) damage_in_table.setModel(damage_in_model) - damage_in_table.expand(damage_in_model.index(0, 0, - damage_in_model.createIndex(0, 0, damage_in_model._root))) + damage_in_root_index = damage_in_model.createIndex(0, 0, damage_in_model._root) + damage_in_table.expand(damage_in_model.index(0, 0, damage_in_root_index)) damage_in_table.sortByColumn(1, Qt.SortOrder.AscendingOrder) damage_in_table.setSelectionModel(TreeSelectionModel(damage_in_model)) heal_out_table = self.widgets.analysis_table_hout - heal_out_model = HealTreeModel(heal_out_item, self.theme_font('tree_table_header'), - self.theme_font('tree_table'), + heal_out_model = HealTreeModel( + heal_out_item, self.theme_font('tree_table_header'), self.theme_font('tree_table'), self.theme_font('', self.theme['tree_table']['::item']['font'])) heal_out_table.setModel(heal_out_model) - heal_out_table.expand(heal_out_model.index(0, 0, - damage_in_model.createIndex(0, 0, heal_out_model._root))) + heal_out_root_index = damage_in_model.createIndex(0, 0, heal_out_model._root) + heal_out_table.expand(heal_out_model.index(0, 0, heal_out_root_index)) heal_out_table.sortByColumn(1, Qt.SortOrder.AscendingOrder) heal_out_table.setSelectionModel(TreeSelectionModel(heal_out_model)) heal_in_table = self.widgets.analysis_table_hin - heal_in_model = HealTreeModel(heal_in_item, self.theme_font('tree_table_header'), - self.theme_font('tree_table'), + heal_in_model = HealTreeModel( + heal_in_item, self.theme_font('tree_table_header'), self.theme_font('tree_table'), self.theme_font('', self.theme['tree_table']['::item']['font'])) heal_in_table.setModel(heal_in_model) - heal_in_table.expand(heal_in_model.index(0, 0, - damage_in_model.createIndex(0, 0, heal_in_model._root))) + heal_in_root_index = damage_in_model.createIndex(0, 0, heal_in_model._root) + heal_in_table.expand(heal_in_model.index(0, 0, heal_in_root_index)) heal_in_table.sortByColumn(1, Qt.SortOrder.AscendingOrder) heal_in_table.setSelectionModel(TreeSelectionModel(heal_in_model)) - + update_shown_columns_dmg(self) update_shown_columns_heal(self) + def update_shown_columns_dmg(self): """ Hides / shows columns of the dmg analysis tables. @@ -207,11 +215,12 @@ def update_shown_columns_dmg(self): for i in range(self.settings.value('dmg_columns_length', type=int)): state = self.settings.value(f'dmg_columns|{i}') if state: - dout_table.showColumn(i+1) - dtaken_table.showColumn(i+1) + dout_table.showColumn(i + 1) + dtaken_table.showColumn(i + 1) else: - dout_table.hideColumn(i+1) - dtaken_table.hideColumn(i+1) + dout_table.hideColumn(i + 1) + dtaken_table.hideColumn(i + 1) + def update_shown_columns_heal(self): """ @@ -222,12 +231,13 @@ def update_shown_columns_heal(self): for i in range(self.settings.value('heal_columns_length', type=int)): state = self.settings.value(f'heal_columns|{i}') if state: - hout_table.showColumn(i+1) - hin_table.showColumn(i+1) + hout_table.showColumn(i + 1) + hin_table.showColumn(i + 1) else: - hout_table.hideColumn(i+1) - hin_table.hideColumn(i+1) - + hout_table.hideColumn(i + 1) + hin_table.hideColumn(i + 1) + + def resize_tree_table(tree): """ Resizes the columns of the given tree table to fit its contents. @@ -239,6 +249,7 @@ def resize_tree_table(tree): width = max(tree.sizeHintForColumn(col), tree.header().sectionSizeHint(col)) + 5 tree.header().resizeSection(col, width) + def copy_analysis_callback(self): """ Callback for copy button on analysis tab @@ -259,7 +270,7 @@ def copy_analysis_callback(self): for selected_cell in selection: column = selected_cell.column() row_name = selected_cell.internalPointer().get_data(0) - if not row_name in selection_dict: + if row_name not in selection_dict: selection_dict[row_name] = dict() if column != 0: cell_data = selected_cell.internalPointer().get_data(column) @@ -272,4 +283,4 @@ def copy_analysis_callback(self): formatted_row_name = ''.join(row_name) if isinstance(row_name, tuple) else row_name output.append(f"{formatted_row_name}: {' | '.join(formatted_row)}") output_string = '\n'.join(output) - self.app.clipboard().setText(output_string) \ No newline at end of file + self.app.clipboard().setText(output_string) diff --git a/OSCRUI/datamodels.py b/OSCRUI/datamodels.py index 5d65e70..09ac7dd 100644 --- a/OSCRUI/datamodels.py +++ b/OSCRUI/datamodels.py @@ -1,17 +1,19 @@ from typing import Iterable -from PySide6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, QAbstractItemModel, QModelIndex -from PySide6.QtCore import QItemSelectionModel, QItemSelection, Slot -from PySide6.QtGui import QFont from OSCR import TreeItem +from PySide6.QtCore import QAbstractItemModel, QAbstractTableModel, QItemSelectionModel +from PySide6.QtCore import QItemSelection, QModelIndex, QSortFilterProxyModel, Qt +from PySide6.QtGui import QFont ARIGHT = Qt.AlignmentFlag.AlignRight ALEFT = Qt.AlignmentFlag.AlignLeft ACENTER = Qt.AlignmentFlag.AlignCenter AVCENTER = Qt.AlignmentFlag.AlignVCenter + class TableModel(QAbstractTableModel): - def __init__(self, data, header: Iterable, index: Iterable, header_font: QFont, cell_font: QFont): + def __init__( + self, data, header: Iterable, index: Iterable, header_font: QFont, cell_font: QFont): """ Creates table model from supplied data. @@ -34,7 +36,7 @@ def rowCount(self, index): def columnCount(self, index): try: - return len(self._data[0]) # all columns must have the same length + return len(self._data[0]) # all columns must have the same length except IndexError: return 0 @@ -56,7 +58,8 @@ def headerData(self, section, orientation, role): if orientation == Qt.Orientation.Vertical: return AVCENTER + ARIGHT - + + class OverviewTableModel(TableModel): """ Model for overview table @@ -75,13 +78,14 @@ def data(self, index, role): elif column in (9, 16, 17, 18, 19, 20, 21, 22): return str(cell) return cell - + if role == Qt.ItemDataRole.FontRole: return self._cell_font if role == Qt.ItemDataRole.TextAlignmentRole: return AVCENTER + ARIGHT - + + class LeagueTableModel(TableModel): """ Model for league table @@ -100,7 +104,7 @@ def data(self, index, role): elif column == 4: return str(cell) return cell - + if role == Qt.ItemDataRole.FontRole: return self._cell_font @@ -108,21 +112,23 @@ def data(self, index, role): if index.column() == 1: return AVCENTER + ALEFT return AVCENTER + ARIGHT - + def headerData(self, section, orientation, role): if role == Qt.ItemDataRole.FontRole and orientation == Qt.Orientation.Vertical: return self._cell_font return super().headerData(section, orientation, role) + class SortingProxy(QSortFilterProxyModel): def __init__(self): super().__init__() def lessThan(self, left, right): - l = self.sourceModel()._data[left.row()][left.column()] - r = self.sourceModel()._data[right.row()][right.column()] - return l > r # inverted operator to make descending sort come up first - + links = self.sourceModel()._data[left.row()][left.column()] + rechts = self.sourceModel()._data[right.row()][right.column()] + return links > rechts # inverted operator to make descending sort come up first + + class TreeModel(QAbstractItemModel): """ Data model for the analysis table @@ -133,12 +139,15 @@ def __init__(self, root_item, header_font: QFont, name_font: QFont, cell_font: Q Parameters: - :param root_item: item supporting the following operations: - - function "get_child(row: int)" returning the n-th child of the item; None if not exists - - function "get_data(column: int)" returning the data of the item at the given column; None if + - function "get_child(row: int)" returning the n-th child of the item; None if not + exists + - function "get_data(column: int)" returning the data of the item at the given column; + None if not exists - - function "append_child(item)" adds the given item as child to the item + - function "append_child(item)" adds the given item as child to the item - property "parent" containing the parent item of the item; None for the root item - - property "row" containing the row number which the item is stored at in its parent item + - property "row" containing the row number which the item is stored at in its parent + item - property "child_count" containing the number of children the item has - property "column_count" containing the number of columns the items data row @@ -163,7 +172,7 @@ def sort(self, column: int, order: Qt.SortOrder): self.recursive_sort(self._root._children[0], column, descending) self.recursive_sort(self._root._children[1], column, descending) self.layoutChanged.emit() - + def recursive_sort(self, item: TreeItem, column: int, desc): if item.child_count > 0: for child in item._children: @@ -178,10 +187,10 @@ def index(self, row: int, column: int, parent: QModelIndex) -> QModelIndex | Non else: p = parent.internalPointer() c = p.get_child(row) - if not c is None: + if c is not None: return self.createIndex(row, column, c) return QModelIndex() - + def parent(self, index: QModelIndex) -> QModelIndex | None: if not index.isValid(): return QModelIndex() @@ -190,24 +199,24 @@ def parent(self, index: QModelIndex) -> QModelIndex | None: if parent is None: return QModelIndex() return self.createIndex(parent.row, 0, parent) - + def rowCount(self, parent: QModelIndex) -> int: if not parent.isValid(): parent = self._root else: parent = parent.internalPointer() return parent.child_count - + def columnCount(self, parent: QModelIndex) -> int: if parent.isValid(): return parent.internalPointer().column_count return self._root.column_count - + def flags(self, index: QModelIndex) -> Qt.ItemFlag: if not index.isValid(): return Qt.ItemFlag.NoItemFlags return super().flags(index) - + def headerData(self, section, orientation, role) -> str: if role == Qt.ItemDataRole.DisplayRole: return self._root.data[section] @@ -255,6 +264,7 @@ def data(self, index: QModelIndex, role: int) -> str: return index.internalPointer().get_data(column) return None + class HealTreeModel(TreeModel): """ Tree Model subclass for the heal tables @@ -291,7 +301,8 @@ def data(self, index: QModelIndex, role: int) -> str: elif role == -13: return index.internalPointer().get_data(column) return None - + + class TreeSelectionModel(QItemSelectionModel): """ Implements custom selection behavior for analysis tables. @@ -299,12 +310,14 @@ class TreeSelectionModel(QItemSelectionModel): def __init__(self, model: QAbstractItemModel): super().__init__(model) - def select(self, index_or_selection: QModelIndex | QItemSelection, + def select( + self, index_or_selection: QModelIndex | QItemSelection, flag: QItemSelectionModel.SelectionFlag): if isinstance(index_or_selection, QItemSelection): try: if index_or_selection.indexes()[0].column() == 0: - super().select(index_or_selection, flag | QItemSelectionModel.SelectionFlag.Rows) + super().select( + index_or_selection, flag | QItemSelectionModel.SelectionFlag.Rows) else: super().select(index_or_selection, flag) # deselecting has empty list of indexes @@ -314,4 +327,4 @@ def select(self, index_or_selection: QModelIndex | QItemSelection, if index_or_selection.isValid(): super().select(index_or_selection, flag) else: - self.clear() \ No newline at end of file + self.clear() diff --git a/OSCRUI/displayer.py b/OSCRUI/displayer.py index ba7ee55..8f8e562 100644 --- a/OSCRUI/displayer.py +++ b/OSCRUI/displayer.py @@ -1,19 +1,21 @@ from typing import Callable, Iterable -from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QTableView, QFrame, QLabel -from pyqtgraph import PlotWidget, BarGraphItem, setConfigOptions, mkPen import numpy as np +from pyqtgraph import BarGraphItem, mkPen, PlotWidget, setConfigOptions +from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QTableView, QVBoxLayout, QWidget from OSCR import TABLE_HEADER from OSCR.combat import Combat from .datamodels import OverviewTableModel, SortingProxy -from .widgetbuilder import SMINMIN, AVCENTER, ACENTER, create_frame, create_label, style_table +from .widgetbuilder import ACENTER, AVCENTER, SMINMIN +from .widgetbuilder import create_frame, create_label, style_table from .widgets import CustomPlotAxis from .style import get_style, theme_font setConfigOptions(antialias=True) + def setup_plot(plot_function: Callable) -> Callable: ''' sets up Plot item and puts it into layout @@ -87,7 +89,8 @@ def create_overview(self): if frame.layout(): QWidget().setLayout(frame.layout()) - time_data, DPS_graph_data, DMG_graph_data, current_table = _create_overview(self.parser1.active_combat) + time_data, DPS_graph_data, DMG_graph_data, current_table = _create_overview( + self.parser1.active_combat) line_layout = create_line_graph(self, DPS_graph_data, time_data) self.widgets.overview_tab_frames[1].setLayout(line_layout) @@ -103,8 +106,9 @@ def create_overview(self): @setup_plot -def create_grouped_bar_plot(self, data: dict[str, tuple], time_reference: dict[str, tuple], - bar_widget: PlotWidget) -> QVBoxLayout: +def create_grouped_bar_plot( + self, data: dict[str, tuple], time_reference: dict[str, tuple], bar_widget: PlotWidget + ) -> QVBoxLayout: """ Creates a bar plot with grouped bars. @@ -129,12 +133,14 @@ def create_grouped_bar_plot(self, data: dict[str, tuple], time_reference: dict[s for (player, graph_data), color, offset in zipper: if player in time_reference: time_data = np.subtract(time_reference[player], offset) - bars = BarGraphItem(x=time_data, width=bar_width, height=graph_data, brush=color, pen=None, + bars = BarGraphItem( + x=time_data, width=bar_width, height=graph_data, brush=color, pen=None, name=player) bar_widget.addItem(bars) legend_data.append((color, player)) return legend_data + @setup_plot def create_horizontal_bar_graph(self, table: list[list], bar_widget: PlotWidget) -> QVBoxLayout: """ @@ -155,14 +161,17 @@ def create_horizontal_bar_graph(self, table: list[list], bar_widget: PlotWidget) x = tuple(line[3] for line in table) y = tuple(range(1, len(x) + 1)) bar_widget.setXRange(0, max(x) * 1.05, padding=0) - bars = BarGraphItem(x0=0, y=y, height=0.75, width=x, brush=self.theme['defaults']['mfg'], pen=None) + bars = BarGraphItem( + x0=0, y=y, height=0.75, width=x, brush=self.theme['defaults']['mfg'], pen=None) bar_widget.addItem(bars) + @setup_plot -def create_line_graph(self, data: dict[str, tuple], time_reference: dict[str, tuple], - graph_widget: PlotWidget) -> QVBoxLayout: +def create_line_graph( + self, data: dict[str, tuple], time_reference: dict[str, tuple], graph_widget: PlotWidget + ) -> QVBoxLayout: """ - Creates line plot from data and returns layout that countins the plot. + Creates line plot from data and returns layout that countins the plot. Parameters: - :param data: dictionary containing the data to be plotted @@ -182,12 +191,13 @@ def create_line_graph(self, data: dict[str, tuple], time_reference: dict[str, tu legend_data.append((color, player)) return legend_data + def create_legend(self, colors_and_names: Iterable[tuple]) -> QFrame: """ Creates Legend from color / name pairs and returns frame containing it. Parameters: - - :param colors_and_names: Iterable containing color / name pairs : [('#9f9f00', 'Line 1'), + - :param colors_and_names: Iterable containing color / name pairs : [('#9f9f00', 'Line 1'), ('#0000ff', 'Line 2'), (...), ...] :return: frame containing the legend @@ -221,6 +231,7 @@ def create_legend(self, colors_and_names: Iterable[tuple]) -> QFrame: frame.setLayout(frame_layout) return frame + def create_legend_item(self, color: str, name: str) -> QFrame: """ Creates a colored patch next to a label inside a frame @@ -240,11 +251,13 @@ def create_legend_item(self, color: str, name: str) -> QFrame: patch_height = self.theme['app']['frame_thickness'] colored_patch.setFixedSize(2*patch_height, patch_height) layout.addWidget(colored_patch, alignment=AVCENTER) - label = create_label(self, name, 'label', style_override={'font': self.theme['plot_legend']['font']}) + label = create_label( + self, name, 'label', style_override={'font': self.theme['plot_legend']['font']}) layout.addWidget(label) frame.setLayout(layout) return frame + def create_overview_table(self, table_data) -> QTableView: """ Creates the overview table and returns it. @@ -253,8 +266,9 @@ def create_overview_table(self, table_data) -> QTableView: """ table_cell_data = tuple(tuple(line[2:]) for line in table_data) table_index = tuple(line[0] + line[1] for line in table_data) - model = OverviewTableModel(table_cell_data, TABLE_HEADER, table_index, - self.theme_font('table_header'), self.theme_font('table')) + model = OverviewTableModel( + table_cell_data, TABLE_HEADER, table_index, self.theme_font('table_header'), + self.theme_font('table')) sort = SortingProxy() sort.setSourceModel(model) table = QTableView(self.widgets.overview_tab_frames[0]) diff --git a/OSCRUI/iofunctions.py b/OSCRUI/iofunctions.py index d21b6b2..563eead 100644 --- a/OSCRUI/iofunctions.py +++ b/OSCRUI/iofunctions.py @@ -6,7 +6,10 @@ from PySide6.QtWidgets import QFileDialog from PySide6.QtGui import QIcon +# -------------------------------------------------------------------------------------------------- # object methods +# -------------------------------------------------------------------------------------------------- + def browse_path(self, default_path: str = None, types: str = 'Any File (*.*)', save=False) -> str: """ @@ -14,7 +17,8 @@ def browse_path(self, default_path: str = None, types: str = 'Any File (*.*)', s Parameters: - :param default_path: path that the file dialog opens at - - :param types: string containing all file extensions and their respective names that are allowed. + - :param types: string containing all file extensions and their respective names that are + allowed. Format: " (*.);; (*.);; [...]" Example: "Logfile (*.log);;Any File (*.*)" """ @@ -30,9 +34,12 @@ def browse_path(self, default_path: str = None, types: str = 'Any File (*.*)', s if not os.path.exists(f): return '' return f - + +# -------------------------------------------------------------------------------------------------- # static functions - +# -------------------------------------------------------------------------------------------------- + + def get_asset_path(asset_name: str, app_directory: str) -> str: """ returns the absolute path to a file in the asset folder @@ -47,6 +54,7 @@ def get_asset_path(asset_name: str, app_directory: str) -> str: else: return '' + def load_icon(filename: str, app_directory: str) -> QIcon: """ Loads icon from path and returns it. @@ -57,6 +65,7 @@ def load_icon(filename: str, app_directory: str) -> QIcon: """ return QIcon(get_asset_path(filename, app_directory)) + def load_icon_series(icons: dict, app_directory: str) -> dict[str, QIcon]: """ Loads multiple icons and returns dictionary containing the icons. @@ -73,6 +82,7 @@ def load_icon_series(icons: dict, app_directory: str) -> dict[str, QIcon]: icon_dict[icon_name] = QIcon(os.path.join(asset_path, file_name)) return icon_dict + def fetch_json(path: str) -> dict | list: """ Fetches json from path and returns dictionary. @@ -86,6 +96,7 @@ def fetch_json(path: str) -> dict | list: data = json.load(file) return data + def store_json(data: dict | list, path: str): """ Stores data to json file at path. Overwrites file at target location. @@ -102,12 +113,13 @@ def store_json(data: dict | list, path: str): except OSError as e: sys.stdout.write(f'[Error] Data could not be saved: {e}') + def sanitize_file_name(txt, chr_set='extended') -> str: """Converts txt to a valid filename. Parameters: - :param txt: The path to convert. - - :param chr_set: + - :param chr_set: - 'printable': Any printable character except those disallowed on Windows/*nix. - 'extended': 'printable' + extended ASCII character codes 128-255 - 'universal': For almost *any* file system. @@ -120,15 +132,16 @@ def sanitize_file_name(txt, chr_set='extended') -> str: white_lists = { 'universal': {'-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'}, 'printable': {chr(x) for x in range(32, 127)} - BLACK_LIST, # 0-32, 127 are unprintable, - 'extended' : {chr(x) for x in range(32, 256)} - BLACK_LIST, + 'extended': {chr(x) for x in range(32, 256)} - BLACK_LIST, } white_list = white_lists[chr_set] result = ''.join(x if x in white_list else FILLER for x in txt) # Step 2: Device names, '.', and '..' are invalid filenames in Windows. - DEVICE_NAMES = ('CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', - 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9', 'CONIN$', - 'CONOUT$', '..', '.') + DEVICE_NAMES = ( + 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', + 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9', + 'CONIN$', 'CONOUT$', '..', '.') if '.' in txt: name, _, ext = result.rpartition('.') ext = f'.{ext}' diff --git a/OSCRUI/leagueconnector.py b/OSCRUI/leagueconnector.py index 63b9297..5d4d06e 100644 --- a/OSCRUI/leagueconnector.py +++ b/OSCRUI/leagueconnector.py @@ -5,8 +5,8 @@ import tempfile import OSCR_django_client -from OSCR.utilities import logline_to_str from OSCR_django_client.api import CombatlogApi, LadderApi, LadderEntriesApi +from OSCR.utilities import logline_to_str from PySide6.QtWidgets import QMessageBox from .datafunctions import CustomThread @@ -58,7 +58,7 @@ def fetch_and_insert_maps(self): def update_ladder_index(self, selected_map): """Open Combat Log Dialog Box""" - if not selected_map in self.league_api.ladder_dict: + if selected_map not in self.league_api.ladder_dict: return selected_ladder = self.league_api.ladder_dict[selected_map] @@ -84,8 +84,8 @@ def update_ladder_index(self, selected_map): ) model = LeagueTableModel( - table_data, LADDER_HEADER, table_index, theme_font(self, "table_header"), theme_font(self, "table") - ) + table_data, LADDER_HEADER, table_index, theme_font(self, "table_header"), + theme_font(self, "table")) sorting_proxy = SortingProxy() sorting_proxy.setSourceModel(model) table = self.widgets.ladder_table diff --git a/OSCRUI/style.py b/OSCRUI/style.py index 902d2a3..9a5d9ec 100644 --- a/OSCRUI/style.py +++ b/OSCRUI/style.py @@ -3,26 +3,27 @@ from PySide6.QtGui import QFont WEIGHT_CONVERSION = { - 'normal': QFont.Weight.Normal, - 'bold': QFont.Weight.Bold, - 'extrabold': QFont.Weight.ExtraBold, - 'medium': QFont.Weight.Medium - } + 'normal': QFont.Weight.Normal, + 'bold': QFont.Weight.Bold, + 'extrabold': QFont.Weight.ExtraBold, + 'medium': QFont.Weight.Medium +} -def get_style(self, widget, override:dict={}) -> str: + +def get_style(self, widget, override: dict = {}) -> str: """ Returns style sheet according to default style of widget with override style. Parameters: - - :param widget: None or str -> name of the widget style in self.theme (may be empty or None if only the - style in override should be applied) + - :param widget: None or str -> name of the widget style in self.theme (may be empty or None if + only the style in override should be applied) - :param override: dict -> contains additional style (optional) :return: str containing css style sheet """ if widget is None or widget == '': return get_css(self, override) - elif widget != 'app' and widget != 'defaults' and widget != 's.c'and widget in self.theme.keys(): + elif widget != 'app' and widget != 'defaults' and widget != 's.c' and widget in self.theme: if len(override) > 0: style = merge_style(self, self.theme[widget], override) else: @@ -30,29 +31,30 @@ def get_style(self, widget, override:dict={}) -> str: return get_css(self, style) -def get_style_class(self, class_name:str, widget, override={}) -> str: +def get_style_class(self, class_name: str, widget, override={}) -> str: """ - Returns style sheet according to default style of widget with override style. Style only applies to - class_name. Sub-controls, pseudo-states and descendant selectors (marked with "~") defined in self.theme - and override are correctly handled. + Returns style sheet according to default style of widget with override style. Style only + applies to class_name. Sub-controls, pseudo-states and descendant selectors (marked with "~") + defined in self.theme and override are correctly handled. Parameters: - :param class_name: str -> name of the widget to be styled - - :param widget: None or str -> name of the widget style in self.theme (may be empty or None if only the - style in override should be applied) + - :param widget: None or str -> name of the widget style in self.theme (may be empty or None if + only the style in override should be applied) - :param override: dict -> contains additional style (optional) :return: str containing css style sheet """ if widget is None or widget == '': style = override - elif widget != 'app' and widget != 'defaults' and widget != 's.c' and widget in self.theme.keys(): + elif widget != 'app' and widget != 'defaults' and widget != 's.c' and widget in self.theme: if len(override) > 0: style = merge_style(self, self.theme[widget], override) else: style = self.theme[widget] else: - raise KeyError(f'Parameter widget=`{widget}` must be None or key of self.theme ' + raise KeyError( + f'Parameter widget=`{widget}` must be None or key of self.theme ' 'except `app` or `defaults`.') main = f'{class_name} {{{get_css(self, style)}}}' for k, v in style.items(): @@ -62,7 +64,8 @@ def get_style_class(self, class_name:str, widget, override={}) -> str: main += f' {get_style_class(self, f"{class_name} {k[1:]}", None, v)}' return main -def merge_style(self, s1:dict, s2:dict) -> dict: + +def merge_style(self, s1: dict, s2: dict) -> dict: """ Returns new dictionary where the given styles are merged. Up to one sub-dictionary is merged recursively. @@ -81,9 +84,11 @@ def merge_style(self, s1:dict, s2:dict) -> dict: result[k] = v return result + def get_css(self, style: dict) -> str: """ - Converts style dictionary into css style sheet. Escapes '@' - shortcuts with their respective values. + Converts style dictionary into css style sheet. Escapes '@' - shortcuts with their respective + values. """ css = str() for key, val in style.items(): @@ -101,9 +106,11 @@ def get_css(self, style: dict) -> str: css += f'{key}:{v};' return css -def theme_font(self, key, font_spec:tuple=()) -> QFont: + +def theme_font(self, key, font_spec: tuple = ()) -> QFont: """ - Returns QFont object with font specified in self.theme or font_spec. Adds default fallback font families. + Returns QFont object with font specified in self.theme or font_spec. Adds default fallback font + families. Parameters: - :param key: key in self.theme to access font tuple like: self.theme[key]['font'] @@ -125,6 +132,7 @@ def theme_font(self, key, font_spec:tuple=()) -> QFont: font_weight = QFont.Weight.Normal return QFont(font_family, font[1], font_weight) + def create_style_sheet(self, d: dict) -> str: """ Creates Stylesheet from dictionary. Dictionary keys represent css selector. diff --git a/OSCRUI/textedit.py b/OSCRUI/textedit.py index 576fa9b..8ff3be6 100644 --- a/OSCRUI/textedit.py +++ b/OSCRUI/textedit.py @@ -2,19 +2,21 @@ from re import sub as re_sub -def clean_player_id(id:str) -> str: +def clean_player_id(id: str) -> str: """ cleans player id and returns handle """ return id[id.find(' ')+1:-1] -def clean_entity_id(id:str) -> str: + +def clean_entity_id(id: str) -> str: """ cleans entity id and returns it """ return re_sub(r'C\[([0-9]+) +?([a-zA-Z_0-9]+)\]', r'\2 \1', id).replace('_', ' ') -def get_entity_num(id:str) -> int: + +def get_entity_num(id: str) -> int: """ gets entity number from entity id """ @@ -26,7 +28,8 @@ def get_entity_num(id:str) -> int: return int(re_sub(r'C\[([0-9]+) +?([a-zA-Z_0-9]+)\]_WCB', r'\1', id)) except TypeError: return -1 - + + def format_damage_tree_data(data, column: int) -> str: """ Formats a data point according to TREE_HEADER @@ -51,7 +54,8 @@ def format_damage_tree_data(data, column: int) -> str: return f'{data:,.0f}' elif column == 19: return f'{data}s' - + + def format_heal_tree_data(data, column: int) -> str: """ Formats a data point according to HEAL_TREE_HEADER @@ -77,7 +81,8 @@ def format_heal_tree_data(data, column: int) -> str: elif column == 11: return f'{data}s' -def compensate_text(text:str) -> str: + +def compensate_text(text: str) -> str: """ Unescapes various characters not correctly represented in combatlog files @@ -95,6 +100,7 @@ def compensate_text(text:str) -> str: text = text.replace('‘', "'") return text + def format_path(path: str): path = path.replace(chr(92), '/') if path[1] == ':' and path[0] >= 'a' and path[0] <= 'z': @@ -104,17 +110,20 @@ def format_path(path: str): path += '/' return path + def format_data(el, integer=False) -> str: """ rounds floats and ints to 2 decimals and sets 1000s seperators, ignores string values - + Parameters: - :param el: value to be formatted - :param integer: rounds numbers to zero decimal places when True (optional) """ if isinstance(el, (int, float)): - if not integer: return f'{el:,.2f}' - else: return f'{el:,.0f}' + if not integer: + return f'{el:,.2f}' + else: + return f'{el:,.0f}' elif isinstance(el, str): el = el.replace('–', '–') el = el.replace('Ãœ', 'Ü') @@ -126,7 +135,8 @@ def format_data(el, integer=False) -> str: return el else: return str(el) - + + def format_datetime_str(datetime: str) -> str: """ Formats datetime string into datetime to be displayed. diff --git a/OSCRUI/widgetbuilder.py b/OSCRUI/widgetbuilder.py index b1ee8f2..c163d76 100644 --- a/OSCRUI/widgetbuilder.py +++ b/OSCRUI/widgetbuilder.py @@ -1,16 +1,17 @@ from types import FunctionType, BuiltinFunctionType, MethodType import os -from PySide6.QtWidgets import QPushButton, QFrame, QLabel, QTreeView, QHeaderView, QTableView, QSpacerItem -from PySide6.QtWidgets import QSizePolicy, QAbstractItemView, QMessageBox, QComboBox, QDialog, QLineEdit -from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QGridLayout, QFileDialog -from PySide6.QtCore import Qt, QSize +from PySide6.QtCore import QSize, Qt from PySide6.QtGui import QIntValidator +from PySide6.QtWidgets import QAbstractItemView, QComboBox, QDialog, QFileDialog, QFrame +from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QHeaderView, QLabel, QLineEdit +from PySide6.QtWidgets import QMessageBox, QPushButton, QSizePolicy, QSpacerItem, QTableView +from PySide6.QtWidgets import QTreeView, QVBoxLayout -from OSCR import split_log_by_lines, split_log_by_combat -from .style import get_style_class, get_style, merge_style, theme_font -from .textedit import format_path from .iofunctions import browse_path +from OSCR import split_log_by_combat, split_log_by_lines +from .style import get_style, get_style_class, merge_style, theme_font +from .textedit import format_path CALLABLE = (FunctionType, BuiltinFunctionType, MethodType) @@ -35,6 +36,7 @@ SMPIXEL = QAbstractItemView.ScrollMode.ScrollPerPixel + def create_button(self, text, style: str = 'button', parent=None, style_override={}, toggle=None): """ Creates a button according to style with parent. @@ -44,8 +46,8 @@ def create_button(self, text, style: str = 'button', parent=None, style_override - :param style: name of the style as in self.theme or style dict - :param parent: parent of the button (optional) - :param style_override: style dict to override default style (optional) - - :param toggle: True or False when button should be a toggle button, None when it should be a normal - button; the bool value indicates the default state of the button + - :param toggle: True or False when button should be a toggle button, None when it should be a + normal button; the bool value indicates the default state of the button :return: configured QPushButton """ @@ -61,8 +63,10 @@ def create_button(self, text, style: str = 'button', parent=None, style_override button.setChecked(toggle) return button -def create_icon_button(self, icon, tooltip: str = '', style: str = 'icon_button', parent=None, - style_override={}) -> QPushButton: + +def create_icon_button( + self, icon, tooltip: str = '', style: str = 'icon_button', parent=None, style_override={} + ) -> QPushButton: """ Creates a button showing an icon according to style with parent. @@ -85,6 +89,7 @@ def create_icon_button(self, icon, tooltip: str = '', style: str = 'icon_button' button.setSizePolicy(SMAXMAX) return button + def create_frame(self, parent=None, style='frame', style_override={}, size_policy=None) -> QFrame: """ Creates a frame with default styling and parent @@ -101,7 +106,8 @@ def create_frame(self, parent=None, style='frame', style_override={}, size_polic frame.setSizePolicy(size_policy if isinstance(size_policy, QSizePolicy) else SMAXMAX) return frame -def create_label(self, text, style:str='', parent=None, style_override={}): + +def create_label(self, text, style: str = '', parent=None, style_override={}): """ Creates a label according to style with parent. @@ -122,8 +128,10 @@ def create_label(self, text, style:str='', parent=None, style_override={}): else: label.setFont(theme_font(self, style)) return label - -def create_button_series(self, parent, buttons:dict, style, shape:str='row', seperator:str='', ret=False): + + +def create_button_series( + self, parent, buttons: dict, style, shape: str = 'row', seperator: str = '', ret=False): """ Creates a row / column of buttons. @@ -131,12 +139,12 @@ def create_button_series(self, parent, buttons:dict, style, shape:str='row', sep - :param parent: widget that will contain the buttons - :param buttons: dictionary containing button details - key "default" contains style override for all buttons (optional) - - all other keys represent one button, key will be the text on the button; value for the key contains - dict with details for the specific button (all optional) + - all other keys represent one button, key will be the text on the button; value for the + key contains dict with details for the specific button (all optional) - "callback": callable that will be called on button click - "style": individual style override dict - - "toggle": True or False when button should be a toggle button, None when it should be a normal - button; the bool value indicates the default state of the button + - "toggle": True or False when button should be a toggle button, None when it should be + a normal button; the bool value indicates the default state of the button - "stretch": stretch value for the button - "align": alignment flag for button - :param style: key for self.theme -> default style @@ -155,15 +163,16 @@ def create_button_series(self, parent, buttons:dict, style, shape:str='row', sep else: shape = 'row' layout = QHBoxLayout() - + layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) button_list = [] - + if seperator != '': - sep_style = {'color':defaults['color'], 'margin':0, 'padding':0, 'background':'rgba(0,0,0,0)'} - + sep_style = { + 'color': defaults['color'], 'margin': 0, 'padding': 0, 'background': '#00000000'} + for i, (name, detail) in enumerate(buttons.items()): if 'style' in detail: button_style = merge_style(self, defaults, detail['style']) @@ -186,9 +195,12 @@ def create_button_series(self, parent, buttons:dict, style, shape:str='row', sep sep_label = self.create_label(seperator, 'label', parent, sep_style) sep_label.setSizePolicy(SMAXMIN) layout.addWidget(sep_label) - - if ret: return layout, button_list - else: return layout + + if ret: + return layout, button_list + else: + return layout + def create_combo_box(self, parent, style: str = 'combobox', style_override: dict = {}) -> QComboBox: """ @@ -210,7 +222,9 @@ def create_combo_box(self, parent, style: str = 'combobox', style_override: dict combo_box.setSizePolicy(SMINMAX) return combo_box -def create_entry(self, default_value, validator=None, style: str = 'entry', style_override: dict = {} + +def create_entry( + self, default_value, validator=None, style: str = 'entry', style_override: dict = {} ) -> QLineEdit: """ Creates an entry widget and styles it. @@ -233,6 +247,7 @@ def create_entry(self, default_value, validator=None, style: str = 'entry', styl entry.setSizePolicy(SMAXMAX) return entry + def resize_tree_table(tree: QTreeView): """ Resizes the columns of the given tree table to fit its contents @@ -243,7 +258,8 @@ def resize_tree_table(tree: QTreeView): for col in range(tree.header().count()): width = max(tree.sizeHintForColumn(col), tree.header().sectionSizeHint(col)) + 5 tree.header().resizeSection(col, width) - + + def create_analysis_table(self, parent, widget) -> QTreeView: """ Creates and returns a QTreeView with parent, styled according to widget. @@ -266,7 +282,6 @@ def create_analysis_table(self, parent, widget) -> QTreeView: table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) table.header().setStyleSheet(get_style_class(self, 'QHeaderView', 'tree_table_header')) table.header().setSectionResizeMode(RFIXED) - #table.header().setSectionsMovable(False) table.header().setMinimumSectionSize(1) table.header().setSectionsClickable(True) table.header().setStretchLastSection(False) @@ -275,6 +290,7 @@ def create_analysis_table(self, parent, widget) -> QTreeView: table.collapsed.connect(lambda: resize_tree_table(table)) return table + def style_table(self, table: QTableView, style_override: dict = {}): """ Styles the given table. @@ -298,6 +314,7 @@ def style_table(self, table: QTableView, style_override: dict = {}): table.verticalHeader().setSectionResizeMode(RFIXED) table.setSizePolicy(SMINMIN) + def show_warning(self, title: str, message: str): """ Displays a warning in form of a message box @@ -307,13 +324,14 @@ def show_warning(self, title: str, message: str): - :param message: message to be displayed """ error = QMessageBox() - error.setIcon(QMessageBox.Icon.Warning), + error.setIcon(QMessageBox.Icon.Warning), error.setText(message) error.setWindowTitle(title) error.setStandardButtons(QMessageBox.StandardButton.Ok) error.setWindowIcon(self.icons['oscr']) error.exec() + def log_size_warning(self): """ Warns user about oversized logfile. @@ -322,9 +340,11 @@ def log_size_warning(self): """ dialog = QMessageBox() dialog.setIcon(QMessageBox.Icon.Warning) - message = ('The combatlog file you are trying to open will impair the performance of the app due to its ' - 'size. It is advised to split the log. \n\nClick "Split Dialog" to split the file, "Cancel" to ' - 'abort combatlog analysis or "Continue" to analyze the log nevertheless.') + message = ( + 'The combatlog file you are trying to open will impair the performance of the app ' + 'due to its size. It is advised to split the log. \n\nClick "Split Dialog" to split ' + 'the file, "Cancel" to abort combatlog analysis or "Continue" to analyze the log ' + 'nevertheless.') dialog.setText(message) dialog.setWindowTitle('Open Source Combalog Reader') dialog.setWindowIcon(self.icons['oscr']) @@ -339,7 +359,8 @@ def log_size_warning(self): return 'continue' else: return 'cancel' - + + def split_dialog(self): """ Opens dialog to split the current logfile. @@ -373,17 +394,18 @@ def split_dialog(self): vertical_layout.addLayout(grid_layout) auto_split_heading = create_label(self, 'Split Log Automatically:', 'label_heading') grid_layout.addWidget(auto_split_heading, 0, 0, alignment=ALEFT) - label_text = ('Automatically splits the logfile at the next combat end after ' - f'{self.settings.value("split_log_after", type=int):,} lines until the entire file has been ' - 'split. The new files are written to the selected folder. It is advised to select an empty ' - 'folder to ensure all files are saved correctly.') + label_text = ( + 'Automatically splits the logfile at the next combat end after ' + f'{self.settings.value("split_log_after", type=int):,} lines until the entire file has ' + ' been split. The new files are written to the selected folder. It is advised to ' + 'select an empty folder to ensure all files are saved correctly.') auto_split_text = create_label(self, label_text, 'label') auto_split_text.setWordWrap(True) auto_split_text.setFixedWidth(self.sidebar_item_width) grid_layout.addWidget(auto_split_text, 1, 0, alignment=ALEFT) auto_split_button = create_button(self, 'Auto Split') auto_split_button.clicked.connect(lambda: auto_split_callback(self, current_logpath)) - grid_layout.addWidget(auto_split_button, 1, 2, alignment=ARIGHT|ABOTTOM) + grid_layout.addWidget(auto_split_button, 1, 2, alignment=ARIGHT | ABOTTOM) grid_layout.setRowMinimumHeight(2, item_spacing) seperator_3 = create_frame(self, content_frame, 'hr', size_policy=SMINMIN) seperator_3.setFixedHeight(self.theme['hr']['height']) @@ -391,10 +413,11 @@ def split_dialog(self): grid_layout.setRowMinimumHeight(4, item_spacing) range_split_heading = create_label(self, 'Export Range of Combats:', 'label_heading') grid_layout.addWidget(range_split_heading, 5, 0, alignment=ALEFT) - label_text = ('Exports combats including and between lower and upper limit to selected file. ' + label_text = ( + 'Exports combats including and between lower and upper limit to selected file. ' 'Both limits refer to the indexed list of all combats in the file starting with 1. ' - 'An upper limit larger than the total number of combats or of "-1", is treated as being equal to ' - 'the total number of combats.') + 'An upper limit larger than the total number of combats or of "-1", is treated as ' + 'being equal to the total number of combats.') range_split_text = create_label(self, label_text, 'label') range_split_text.setWordWrap(True) range_split_text.setFixedWidth(self.sidebar_item_width) @@ -412,7 +435,8 @@ def split_dialog(self): lower_validator.setBottom(1) lower_range_entry.setValidator(lower_validator) lower_range_entry.setText('1') - lower_range_entry.setStyleSheet(get_style(self, 'entry', {'margin-top': 0, 'margin-left': '@csp'})) + lower_range_entry.setStyleSheet( + get_style(self, 'entry', {'margin-top': 0, 'margin-left': '@csp'})) lower_range_entry.setFixedWidth(self.sidebar_item_width // 7) range_limit_layout.addWidget(lower_range_entry, 1, 1, alignment=AVCENTER) upper_range_entry = QLineEdit() @@ -420,14 +444,16 @@ def split_dialog(self): upper_validator.setBottom(-1) upper_range_entry.setValidator(upper_validator) upper_range_entry.setText('1') - upper_range_entry.setStyleSheet(get_style(self, 'entry', {'margin-top': 0, 'margin-left': '@csp'})) + upper_range_entry.setStyleSheet( + get_style(self, 'entry', {'margin-top': 0, 'margin-left': '@csp'})) upper_range_entry.setFixedWidth(self.sidebar_item_width // 7) range_limit_layout.addWidget(upper_range_entry, 2, 1, alignment=AVCENTER) grid_layout.addLayout(range_limit_layout, 6, 1) range_split_button = create_button(self, 'Export Combats') - range_split_button.clicked.connect(lambda le=lower_range_entry, ue=upper_range_entry: + range_split_button.clicked.connect( + lambda le=lower_range_entry, ue=upper_range_entry: combat_split_callback(self, current_logpath, le.text(), ue.text())) - grid_layout.addWidget(range_split_button, 6, 2, alignment=ARIGHT|ABOTTOM) + grid_layout.addWidget(range_split_button, 6, 2, alignment=ARIGHT | ABOTTOM) content_frame.setLayout(vertical_layout) @@ -438,19 +464,24 @@ def split_dialog(self): dialog.setSizePolicy(SMAXMAX) dialog.exec() + def auto_split_callback(self, path: str): """ Callback for auto split button """ - folder_path = QFileDialog.getExistingDirectory(self.window, 'Select Folder', os.path.dirname(path)) - split_log_by_lines(path, folder_path, self.settings.value('split_log_after', type=int), + folder_path = QFileDialog.getExistingDirectory( + self.window, 'Select Folder', os.path.dirname(path)) + split_log_by_lines( + path, folder_path, self.settings.value('split_log_after', type=int), self.settings.value('combat_distance', type=int)) + def combat_split_callback(self, path: str, first_num: str, last_num: str): """ Callback for combat split button """ target_path = browse_path(self, path, 'Logfile (*.log);;Any File (*.*)', True) - split_log_by_combat(path, target_path, int(first_num), int(last_num), - self.settings.value('seconds_between_combats', type=int), - self.settings.value('excluded_event_ids', type=list)) \ No newline at end of file + split_log_by_combat( + path, target_path, int(first_num), int(last_num), + self.settings.value('seconds_between_combats', type=int), + self.settings.value('excluded_event_ids', type=list)) diff --git a/OSCRUI/widgets.py b/OSCRUI/widgets.py index 96b5d83..869ea8c 100644 --- a/OSCRUI/widgets.py +++ b/OSCRUI/widgets.py @@ -1,11 +1,13 @@ -from PySide6.QtWidgets import QWidget, QPushButton, QTabWidget, QFrame, QTreeView, QComboBox, QTableView -from PySide6.QtGui import QIcon, QPixmap, QPainter, QFont -from PySide6.QtCore import QRect, Slot -from pyqtgraph import AxisItem, PlotWidget, BarGraphItem import numpy as np +from pyqtgraph import AxisItem, BarGraphItem, PlotWidget +from PySide6.QtCore import QRect, Slot +from PySide6.QtGui import QIcon, QPixmap, QPainter, QFont +from PySide6.QtWidgets import QComboBox, QFrame, QPushButton, QTableView, QTabWidget, QTreeView +from PySide6.QtWidgets import QWidget from .widgetbuilder import SMINMIN + class WidgetStorage(): """ Class to store widgets. @@ -23,7 +25,7 @@ def __init__(self): self.overview_menu_buttons: list[QPushButton] = list() self.overview_tabber: QTabWidget self.overview_tab_frames: list[QFrame] = list() - + self.analysis_menu_buttons: list[QPushButton] = list() self.analysis_copy_combobox: QComboBox self.analysis_tabber: QTabWidget @@ -39,12 +41,13 @@ def __init__(self): self.ladder_map: QComboBox self.ladder_table: QTableView - + @property def analysis_table(self): return (self.analysis_table_dout, self.analysis_table_dtaken, self.analysis_table_hout, self.analysis_table_hin) + class FlipButton(QPushButton): """ QPushButton with two sets of commands, texts and icons that alter on click. @@ -74,12 +77,12 @@ def flip(self): self.setText(self._r_text) self._r = not self._r - def set_icon_r(self, icon:QIcon): + def set_icon_r(self, icon: QIcon): self._r_icon = icon if self._r: self.setIcon(icon) - def set_icon_l(self, icon:QIcon): + def set_icon_l(self, icon: QIcon): self._l_icon = icon if not self._r: self.setIcon(icon) @@ -99,7 +102,7 @@ def set_func_r(self, func): def set_func_l(self, func): self._l_function = func - + def configure(self, settings): self.set_icon_r(settings['icon_r']) self.set_icon_l(settings['icon_l']) @@ -109,15 +112,17 @@ def configure(self, settings): def _f(self): return + class BannerLabel(QWidget): """ - Label displaying image that resizes according to its parents width while preserving aspect ratio. + Label displaying image that resizes according to its parents width while preserving aspect + ratio. """ def __init__(self, path, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.setPixmap(QPixmap(path)) self.setSizePolicy(SMINMIN) - self.setMinimumHeight(10) # forces visibility + self.setMinimumHeight(10) # forces visibility def setPixmap(self, p): self.p = p @@ -134,6 +139,7 @@ def paintEvent(self, event): self.setMaximumHeight(h) self.setMinimumHeight(h) + class CustomPlotAxis(AxisItem): """ Extending AxisItem for custom tick formatting @@ -145,7 +151,7 @@ def __init__(self, *args, unit: str = '', **kwargs): @property def unit(self): return self._unit - + @unit.setter def unit(self, value): self._unit = ' ' + value @@ -153,7 +159,7 @@ def unit(self, value): def tickStrings(self, values, scale, spacing): if self.logMode: return self.logTickStrings(values, scale, spacing) - + strings = list() for tick in values: if tick >= 1000000: @@ -163,7 +169,8 @@ def tickStrings(self, values, scale, spacing): else: strings.append(f'{tick:.0f}{self._unit}') return strings - + + class AnalysisPlot(PlotWidget): """ PlotWidget for plotting the analysis plot. @@ -197,7 +204,8 @@ def __init__(self, colors: tuple, tick_color: str, tick_font: QFont, legend_layo def add_bar(self, data): """ - Adds plot item to plot widget and removes plot item if there are more than 5 currently displayed. + Adds plot item to plot widget and removes plot item if there are more than 5 currently + displayed. Parameters: - :param data: list or array containing the height of the bars @@ -221,10 +229,10 @@ def add_bar(self, data): self._bar_queue.append(bars) self.addItem(bars) self._bar_position += 1 - if self._bar_position >=5: + if self._bar_position >= 5: self._bar_position = 0 return brush_color - + def add_legend_item(self, legend_item: QFrame): self._legend_queue.append(legend_item) self._legend_layout.addWidget(legend_item) @@ -241,7 +249,7 @@ def clear_plot(self): legend_item.setParent(None) self._legend_queue = list() self._bar_position = 0 - + def toggle_freeze(self, state): """ Freezes when unfrozen, unfreezes when frozen diff --git a/main.py b/main.py index f489e55..ae31a37 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,9 @@ -import sys import os +import sys from OSCRUI import OSCRUI + class Launcher(): version = '2024.03a042' @@ -14,7 +15,7 @@ class Launcher(): 'bg': '#1a1a1a', 'fg': '#eeeeee', 'oscr': '#c82934', - 'font': ('Overpass', 11, 'normal'), # used when no font is specified in style definition + 'font': ('Overpass', 11, 'normal'), 'heading': ('Overpass', 14, 'bold'), 'text': ('Overpass', 11, 'normal'), 'subhead': ('Overpass', 12, 'medium'), @@ -33,7 +34,7 @@ class Launcher(): 'width': 8, }, 'QScrollBar:horizontal': { - 'height': 8, + 'height': 8, }, # space above and below the scrollbar handle 'QScrollBar::add-page, QScrollBar::sub-page': { @@ -47,7 +48,7 @@ class Launcher(): }, # scroll bar arrow buttons 'QScrollBar::add-line, QScrollBar::sub-line': { - 'height': 0 # hiding the arrow buttons + 'height': 0 # hiding the arrow buttons }, # top left corner of table 'QTableCornerButton::section': { @@ -57,21 +58,21 @@ class Launcher(): }, # shortcuts, @bg -> means bg in this sub-dictionary 'defaults': { - 'bg': '#1a1a1a', # background - 'mbg': '#242424', # medium background - 'lbg': '#404040', # light background - 'oscr': '#c82934', # accent - 'loscr': '#20c82934', # light accent (12.5% opacity) + 'bg': '#1a1a1a', # background + 'mbg': '#242424', # medium background + 'lbg': '#404040', # light background + 'oscr': '#c82934', # accent + 'loscr': '#20c82934', # light accent (12.5% opacity) 'font': ('Overpass', 11, 'normal'), - 'fg': '#eeeeee', # foreground (usually text) - 'mfg': '#bbbbbb', # medium foreground - 'bc': '#888888', # border color - 'bw': 1, # border width - 'br': 2, # border radius - 'sep': 2, # seperator -> width of major seperating lines - 'margin': 10, # default margin between widgets - 'csp': 5, # child spacing -> content margin - 'isp': 15, # item spacing + 'fg': '#eeeeee', # foreground (usually text) + 'mfg': '#bbbbbb', # medium foreground + 'bc': '#888888', # border color + 'bw': 1, # border width + 'br': 2, # border radius + 'sep': 2, # seperator -> width of major seperating lines + 'margin': 10, # default margin between widgets + 'csp': 5, # child spacing -> content margin + 'isp': 15, # item spacing }, # dark frame 'frame': { @@ -96,7 +97,7 @@ class Launcher(): 'label': { 'color': '@fg', 'margin': (3, 0, 3, 0), - 'qproperty-indent': '0', # disables auto-indent + 'qproperty-indent': '0', # disables auto-indent 'border-style': 'none', 'font': ('Overpass', 11, 'normal') }, @@ -159,7 +160,7 @@ class Launcher(): 'menu_button': { 'background': 'none', 'color': '@fg', - 'text-decoration': 'none', # removes underline + 'text-decoration': 'none', # removes underline 'border': 'none', 'margin': (6, 10, 4, 10), # 'margin-left': 10, @@ -220,7 +221,8 @@ class Launcher(): 'border-radius': '@br', 'margin-top': '@csp', 'font': ('Overpass', 10, 'normal'), - ':focus': { # cursor is inside the line + # cursor is inside the line + ':focus': { 'border-color': '@oscr' } }, @@ -241,7 +243,7 @@ class Launcher(): 'border-radius': '@br', 'margin': 0, 'font': ('Overpass', 10, 'normal'), - 'outline': '0', # removes dotted line around clicked item + 'outline': '0', # removes dotted line around clicked item '::item': { 'border-width': '@bw', 'border-style': 'solid', @@ -256,7 +258,7 @@ class Launcher(): }, # selected but not the last click of the user '::item:selected:!active': { - 'color':'@fg' + 'color': '@fg' }, '::item:hover': { 'background-color': '@loscr', @@ -301,15 +303,16 @@ class Launcher(): 'color': '@bc' } }, - # table; ::item refers to the cells, :alternate is the alternate style -> s.c: table_alternate + # table; ::item refers to the cells, + # :alternate is the alternate style -> s.c: table_alternate 'table': { 'color': '@fg', 'background-color': '@bg', 'border-width': '@bw', 'border-style': 'solid', 'border-color': '@bc', - 'gridline-color': 'rgba(0,0,0,0)', # -> s.c: table_gridline - 'outline': '0', # removes dotted line around clicked item + 'gridline-color': 'rgba(0,0,0,0)', # -> s.c: table_gridline + 'outline': '0', # removes dotted line around clicked item 'margin': (0, 10, 10, 0), 'font': ('Roboto Mono', 12, 'Medium'), '::item': { @@ -360,12 +363,12 @@ class Launcher(): 'border-bottom-width': '@sep', 'border-bottom-style': 'solid', 'border-bottom-color': '@bc', - 'outline': '0', # removes dotted line around clicked item + 'outline': '0', # removes dotted line around clicked item 'font': ('Overpass', 12, 'Medium'), '::section': { 'background-color': '@mbg', 'color': '@fg', - 'padding': (0, 0, 0, 0), #(0, -8, -3, 6), # don't ask + 'padding': (0, 0, 0, 0), # (0, -8, -3, 6), # don't ask 'border': 'none', 'margin': 0 }, @@ -381,7 +384,7 @@ class Launcher(): 'border-right-width': '@sep', 'border-right-style': 'solid', 'border-right-color': '@bc', - 'outline': '0', # removes dotted line around clicked item + 'outline': '0', # removes dotted line around clicked item '::section': { 'background-color': '@mbg', 'color': '@fg', @@ -393,14 +396,15 @@ class Launcher(): 'background-color': '@loscr' }, }, - # analysis table; ::item refers to the cells; ::branch refers to the space on the left of the rows + # analysis table; ::item refers to the cells; + # ::branch refers to the space on the left of the rows 'tree_table': { 'border': '1px solid #888888', 'background-color': '@bg', 'alternate-background-color': '@mbg', 'color': '@fg', 'margin': (10, 10, 10, 0), - 'outline': '0', # removes dotted line around clicked item + 'outline': '0', # removes dotted line around clicked item 'font': ('Overpass', 12, 'Normal'), '::item': { 'font': ('Roboto Mono', 12, 'Normal'), @@ -583,7 +587,7 @@ def app_config() -> dict: 'heal_columns|10': True, 'heal_columns|11': True, 'heal_columns|12': True, - 'heal_columns_length' : 13, + 'heal_columns_length': 13, 'split_log_after': 480000, 'seconds_between_combats': 100, 'excluded_event_ids': ['Autodesc.Combatevent.Falling', ''], @@ -596,9 +600,8 @@ def app_config() -> dict: @staticmethod def launch(): args = {} - exit_code = OSCRUI(version=Launcher.version, theme=Launcher.theme, args=args, - path=Launcher.base_path(), config=Launcher.app_config() - ).run() + exit_code = OSCRUI(version=Launcher.version, theme=Launcher.theme, args=args, + path=Launcher.base_path(), config=Launcher.app_config()).run() sys.exit(exit_code)