def _make_widgets(self): bfr1 = tkd.LabelFrame(self, height=28) bfr1.propagate(False) bfr1.pack(expand=False, fill=tk.X, padx=1, pady=1) tkd.Button(bfr1, text='Sync', width=6, command=self.sync_local_grail, relief=tk.RIDGE, borderwidth=1, tooltip='Updates your local grail to include items logged either\nin your profiles or on herokuapp').pack(side=tk.LEFT, padx=[1, 15], pady=1) tkd.Checkbutton(bfr1, text='Profiles', variable=self.sync_drops).pack(side=tk.LEFT) tkd.Checkbutton(bfr1, text='Herokuapp', variable=self.sync_herokuapp).pack(side=tk.LEFT) bfr2 = tkd.Frame(self) bfr2.pack(pady=12, expand=True, fill=tk.X) tkd.Button(bfr2, text='Upload grail to herokuapp', command=self.upload_to_herokuapp, borderwidth=3, tooltip='This will not delete already found items on herokuapp if they are not\nin your local grail, but only add new items').pack(padx=8, side=tk.LEFT, fill=tk.X, expand=True) bfr3 = tkd.Frame(self) bfr3.pack(side=tk.BOTTOM, expand=tk.YES, fill=tk.X) tkd.Button(bfr3, text='Item table', borderwidth=3, command=self.open_grail_table, width=1).pack(side=tk.LEFT, fill=tk.X, padx=[1, 0], pady=1, expand=tk.YES) tkd.Button(bfr3, text='Grail controller', borderwidth=3, command=self.open_grail_controller, width=1).pack(side=tk.LEFT, fill=tk.X, padx=1, pady=1, expand=tk.YES) descr = tkd.ListboxFrame(self) descr.propagate(False) tk.Grid.columnconfigure(descr, 0, weight=1) descr.pack(side=tk.BOTTOM, fill=tk.X, expand=False) for i, l in enumerate(['', 'Exist', 'Owned', 'Left', '%']): tkd.ListboxLabel(descr, text=l).grid(row=0, column=i) ttk.Separator(descr, orient=tk.HORIZONTAL).grid(row=1, column=0, columnspan=5, sticky='ew') self._make_row(descr, 2, 'Uniq Armor') self._make_row(descr, 3, 'Uniq Weapons') self._make_row(descr, 4, 'Uniq Other') self._make_row(descr, 5, 'Sets') self._make_row(descr, 6, 'Runes') self._make_row(descr, 7, 'Total')
def run_table(self, laps): run_table_fr = tkd.Frame(self.tabcontrol) self.tabcontrol.add(run_table_fr, text='Run table') cols = [ "Run", "Run time", "Real time", "Name", "MF", "Players X", "Level", "XP Gained", "Uniques kills", "Champions kills", "Minion kills", "Total kills", "Session", "Map seed" ] tree_frame = tkd.Frame(run_table_fr) btn_frame2 = tkd.Frame(run_table_fr) btn_frame2.pack(side=tk.BOTTOM) vscroll_tree = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL) hscroll_tree = ttk.Scrollbar(run_table_fr, orient=tk.HORIZONTAL) tree = tkd.Treeview(tree_frame, selectmode=tk.BROWSE, yscrollcommand=vscroll_tree.set, xscrollcommand=hscroll_tree.set, show='headings', columns=cols, alternate_colour=True) hscroll_tree.config(command=tree.xview) vscroll_tree.config(command=tree.yview) tkd.Button(btn_frame2, text='Save as .csv', command=lambda: self.save_to_csv(tree)).pack(side=tk.LEFT, fill=tk.X) vscroll_tree.pack(side=tk.RIGHT, fill=tk.Y) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) hscroll_tree.pack(side=tk.BOTTOM, fill=tk.X) tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) renamed_cols = [ c.replace('Uniques', 'Unique').replace('Champions', 'Champion') for c in cols ] tree['columns'] = renamed_cols widths = [35, 60, 115, 60, 42, 58, 45, 75, 71, 89, 71, 59, 80, 70] for i, col in enumerate(renamed_cols): tree.column(col, stretch=tk.NO, minwidth=0, width=widths[i]) if col in [ 'Run', 'XP Gained', 'Champion kills', 'Unique kills', 'Minion kills', 'Total kills' ]: sort_by = 'num' else: sort_by = 'name' tree.heading(col, text=col, sort_by=sort_by) for n, lap in enumerate(laps, 1): tmp_lap = dict(lap) tmp_lap['Run time'] = other_utils.build_time_str( tmp_lap['Run time']) tmp_lap['Run'] = n tree.insert('', tk.END, values=[tmp_lap.get(col, '') for col in cols])
def map_evaluation(self, laps): map_eval_fr = tkd.Frame(self.tabcontrol) self.tabcontrol.add(map_eval_fr, text='Map evaluation') cols = [ "Map", "Map seed", "Run count", "Avg run time", "Avg MF", "Avg players X", "Avg packs (55% hork)", "Avg secs/pack (55% hork)", "Adjeff (55% hork)" ] tree_frame = tkd.Frame(map_eval_fr) btn_frame2 = tkd.Frame(map_eval_fr) btn_frame2.pack(side=tk.BOTTOM) vscroll_tree = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL) hscroll_tree = ttk.Scrollbar(map_eval_fr, orient=tk.HORIZONTAL) tree = tkd.Treeview(tree_frame, selectmode=tk.BROWSE, yscrollcommand=vscroll_tree.set, xscrollcommand=hscroll_tree.set, show='headings', columns=cols, alternate_colour=True) hscroll_tree.config(command=tree.xview) vscroll_tree.config(command=tree.yview) tkd.Button(btn_frame2, text='Save as .csv', command=lambda: self.save_to_csv(tree)).pack(side=tk.LEFT, fill=tk.X) vscroll_tree.pack(side=tk.RIGHT, fill=tk.Y) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) hscroll_tree.pack(side=tk.BOTTOM, fill=tk.X) tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) tree['columns'] = cols widths = [35, 70, 70, 80, 55, 85, 130, 150, 105] for i, col in enumerate(cols): tree.column(col, stretch=tk.NO, minwidth=0, width=widths[i]) if col in [ "Avg packs (55% hork)", "Avg secs/pack (55% hork)", "Adjeff (55% hork)" ]: sort_by = 'name' else: sort_by = 'num' tree.heading(col, text=col, sort_by=sort_by) grouped = self.group_laps(laps=laps) for n, smap in enumerate(grouped, 1): tmp_lap = dict(smap) tmp_lap['Map'] = n tree.insert('', tk.END, values=[tmp_lap.get(col, '') for col in cols])
def _make_widgets(self): tkd.Label(self, text='Select active profile', justify=tk.LEFT).pack(anchor=tk.W) profile_dropdown_frame = tkd.Frame(self, height=28, width=238, pady=2, padx=2) profile_dropdown_frame.propagate(False) profile_dropdown_frame.pack() self.active_profile.set(self.main_frame.active_profile) self.profile_dropdown = tkd.Combobox(profile_dropdown_frame, textvariable=self.active_profile, state='readonly', values=self.main_frame.profiles) self.profile_dropdown.bind("<<ComboboxSelected>>", lambda e: self._change_active_profile()) self.profile_dropdown.bind("<FocusOut>", lambda e: self.profile_dropdown.selection_clear()) self.profile_dropdown.pack(side=tk.LEFT, expand=True, fill=tk.X) tkd.Button(profile_dropdown_frame, text='New...', command=self._add_new_profile).pack(side=tk.LEFT) tkd.Button(profile_dropdown_frame, text='Delete', command=self._delete_profile).pack(side=tk.LEFT) self.run_type = tk.StringVar(self, value=self.extra_data.get('Run type', '')) self.game_mode = tk.StringVar(self, value=self.extra_data.get('Game mode', 'Single Player')) self.char_name = tk.StringVar(self, value=self.extra_data.get('Character name', '')) self._extra_info_label('Run type', self.run_type) # self._extra_info_label('Game mode', self.game_mode) self._extra_info_label('Character name', self.char_name) tkd.Label(self, text='Select an archived run for this profile', justify=tk.LEFT).pack(anchor=tk.W, pady=(6, 0)) sel_frame = tkd.Frame(self, height=28, width=238, pady=2, padx=2) sel_frame.propagate(False) sel_frame.pack() self.archive_dropdown = tkd.Combobox(sel_frame, textvariable=self.selected_archive, state='readonly', values=self.available_archive) self.archive_dropdown.bind("<<ComboboxSelected>>", lambda e: self.update_descriptive_statistics()) self.archive_dropdown.bind("<FocusOut>", lambda e: self.archive_dropdown.selection_clear()) self.archive_dropdown.pack(side=tk.LEFT, expand=True, fill=tk.X) tkd.Button(sel_frame, text='Open', command=lambda: archive_browser.ArchiveBrowser(self.main_frame)).pack(side=tk.LEFT) tkd.Button(sel_frame, text='Delete', command=self.delete_archived_session).pack(side=tk.LEFT) self.descr = tkd.Listbox(self, selectmode=tk.EXTENDED, height=8, activestyle='none', font=('courier', 8)) self.descr.bind('<FocusOut>', lambda e: self.descr.selection_clear(0, tk.END)) self.descr.pack(side=tk.BOTTOM, fill=tk.X, expand=1, anchor=tk.S)
def statistics(self, laps, drops, session_time): statistics_fr = tkd.Frame(self.tabcontrol) self.tabcontrol.add(statistics_fr, text='Statistics') sum_laps = sum(x['Run time'] if isinstance(x, dict) else x for x in laps) avg_lap = sum_laps / len(laps) if laps else 0 pct = sum_laps * 100 / session_time if session_time > 0 else 0 # Kill averages list_uniques = [ int(x.get('Uniques kills', '')) for x in laps if isinstance(x, dict) and x.get('Uniques kills', '') ] list_champs = [ int(x.get('Champions kills', '')) for x in laps if isinstance(x, dict) and x.get('Uniques kills', '') ] avg_uniques = sum(list_uniques) / len( list_uniques) if list_uniques else 0 avg_champs = sum(list_champs) / len(list_champs) if list_champs else 0 avg_packs = avg_uniques + avg_champs / 2.534567 seconds_per_pack = avg_lap / avg_packs if avg_packs > 0 else 0 # Configure the list frame with scrollbars which displays the archive of the chosen session list_win = tkd.Frame(statistics_fr) list_frame = tkd.Frame(list_win) vscroll = ttk.Scrollbar(list_frame, orient=tk.VERTICAL) hscroll = ttk.Scrollbar(list_win, orient=tk.HORIZONTAL) txt_list = tkd.Text(list_frame, yscrollcommand=vscroll.set, xscrollcommand=hscroll.set, font='courier 10', wrap=tk.WORD, state=tk.NORMAL, cursor='', exportselection=1, name='archivebrowser') vscroll.pack(side=tk.RIGHT, fill=tk.Y) txt_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) txt_list.tag_configure("HEADER", font=font.Font(family='courier', size=12, weight='bold', underline=True)) hscroll.config(command=txt_list.xview) vscroll.config(command=txt_list.yview) # Build header for output file with information and descriptive statistics txt_list.insert(tk.END, 'Statistics', tag='HEADER') txt_list.insert( tk.END, '\nCharacter name: %s' % self.main_frame.profile_tab.extra_data.get('Character name', '')) txt_list.insert( tk.END, '\nRun type: %s' % self.main_frame.profile_tab.extra_data.get('Run type', '')) txt_list.insert( tk.END, '\nGame mode: %s' % self.main_frame.profile_tab.extra_data.get('Game mode', 'Single Player')) txt_list.insert( tk.END, '\n\nTotal session time: %s' % other_utils.build_time_str(session_time)) txt_list.insert( tk.END, '\nTotal run time: %s' % other_utils.build_time_str(sum_laps)) txt_list.insert( tk.END, '\nAverage run time: %s' % other_utils.build_time_str(avg_lap)) txt_list.insert( tk.END, '\nFastest run time: %s' % other_utils.build_time_str( min([ x['Run time'] if isinstance(x, dict) else x for x in laps ], default=0))) txt_list.insert(tk.END, '\nNumber of runs: %s' % str(len(laps))) txt_list.insert(tk.END, '\nTime spent in runs: %s%%' % str(round(pct, 2))) txt_list.insert( tk.END, '\n\nAvg unique kills: %s' % str(round(avg_uniques, 2))) txt_list.insert( tk.END, '\nAvg champion kills: %s' % str(round(avg_champs, 2))) txt_list.insert( tk.END, '\nAvg pack kills: %s' % str(round(avg_packs, 2))) txt_list.insert( tk.END, '\nAvg seconds/pack: %s' % str(round(seconds_per_pack, 2))) # List all drops collected if drops: if any(drop for drop in drops.values()): txt_list.insert(tk.END, '\n\nCollected drops', tag='HEADER') for run_no, drop in drops.items(): if drop: str_n = ' ' * max( len(str(len(laps))) - len(str(run_no)), 0) + str(run_no) txt_list.insert( tk.END, '\nRun ' + str_n + ' - ' + ', '.join(x['input'].strip() for x in drop)) if laps: txt_list.insert(tk.END, '\n\nRun times', tag='HEADER') # Loop through all runs and add run times and drops for each run for n, lap in enumerate(laps, 1): run_time = lap['Run time'] if isinstance(lap, dict) else lap str_n = ' ' * max(len(str(len(laps))) - len(str(n)), 0) + str(n) droplst = drops.get(str(n), []) tmp = '\nRun ' + str_n + ': ' + other_utils.build_time_str( run_time) if droplst: tmp += ' - ' + ', '.join([d['input'].strip() for d in droplst]) txt_list.insert(tk.END, tmp) # Add bold tags # txt_list.tag_add("BOLD", "1.0", "1.15") # txt_list.tag_add("BOLD", "2.0", "2.9") # txt_list.tag_add("BOLD", "3.0", "3.10") # txt_list.tag_add("BOLD", "5.0", "5.19") # txt_list.tag_add("BOLD", "6.0", "6.15") # txt_list.tag_add("BOLD", "7.0", "7.17") # txt_list.tag_add("BOLD", "8.0", "8.17") # txt_list.tag_add("BOLD", "9.0", "9.15") # txt_list.tag_add("BOLD", "10.0", "10.19") # txt_list.tag_add("BOLD", "1.16", "1.0 lineend") # txt_list.tag_add("BOLD", "2.16", "2.0 lineend") # txt_list.tag_add("BOLD", "3.16", "3.0 lineend") # txt_list.tag_add("BOLD", "5.20", "5.0 lineend") # txt_list.tag_add("BOLD", "6.20", "6.0 lineend") # txt_list.tag_add("BOLD", "7.20", "7.0 lineend") # txt_list.tag_add("BOLD", "8.20", "8.0 lineend") # txt_list.tag_add("BOLD", "9.20", "9.0 lineend") # txt_list.tag_add("BOLD", "10.20", "10.0 lineend") # txt_list.tag_add("HEADER", "12.0", "12.0 lineend") txt_list.config(state=tk.DISABLED) btn_frame1 = tkd.Frame(statistics_fr) tkd.Button(btn_frame1, text='Copy to clipboard', command=lambda: self.copy_to_clipboard( txt_list.get(1.0, tk.END))).pack(side=tk.LEFT, fill=tk.X) tkd.Button( btn_frame1, text='Save as .txt', command=lambda: self.save_to_txt(txt_list.get(1.0, tk.END))).pack( side=tk.LEFT, fill=tk.X) # Packs all the buttons and UI in the archive browser. Packing order is very important: # TOP: Title first (furthest up), then list frame # BOTTOM: Buttons first (furthest down) and then horizontal scrollbar list_win.pack(side=tk.TOP, fill=tk.BOTH, expand=1) list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1) hscroll.pack(side=tk.BOTTOM, fill=tk.X) btn_frame1.pack(side=tk.BOTTOM)
def drop_table(self, drops): drop_table_fr = tkd.Frame(self.tabcontrol) self.tabcontrol.add(drop_table_fr, text='Drop table') cols = [ "Run", "Item name", "Extra input", "Real time", "TC", "QLVL", "Item Class", "Grailer", "Eth Grailer" ] tree_frame = tkd.Frame(drop_table_fr) btn_frame2 = tkd.Frame(drop_table_fr) btn_frame2.pack(side=tk.BOTTOM) vscroll_tree = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL) hscroll_tree = ttk.Scrollbar(drop_table_fr, orient=tk.HORIZONTAL) tree = tkd.Treeview(tree_frame, selectmode=tk.BROWSE, yscrollcommand=vscroll_tree.set, xscrollcommand=hscroll_tree.set, show='headings', columns=cols, alternate_colour=True) hscroll_tree.config(command=tree.xview) vscroll_tree.config(command=tree.yview) tkd.Button(btn_frame2, text='Save as .csv', command=lambda: self.save_to_csv(tree)).pack(side=tk.LEFT, fill=tk.X) vscroll_tree.pack(side=tk.RIGHT, fill=tk.Y) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) hscroll_tree.pack(side=tk.BOTTOM, fill=tk.X) tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) tree['columns'] = cols widths = [35, 190, 140, 120, 35, 35, 100, 47, 58] tree.tag_configure('Grail', background='#e6ffe6') for i, col in enumerate(cols): tree.column(col, stretch=tk.NO, minwidth=0, width=widths[i]) if col in ['Run', 'TC', 'QLVL']: sort_by = 'num' else: sort_by = 'name' tree.heading(col, text=col, sort_by=sort_by) for n, drop_list in drops.items(): for drop in drop_list: tmp_drop = dict(drop) tmp_drop['Run'] = n if drop.get("item_name", ''): tmp_drop["Item name"] = drop["item_name"] tmp_drop["Extra input"] = drop["extra"] else: tmp_drop["Item name"] = tmp_drop["input"] tmp_drop["Extra input"] = "" if drop.get('Grailer', False) == 'True' or drop.get( 'Eth Grailer', False) == 'True': tree.insert('', tk.END, values=[tmp_drop.get(col, '') for col in cols], tag='Grail') else: tree.insert('', tk.END, values=[tmp_drop.get(col, '') for col in cols])
def drop_table(self, drops): flat_drops = [{ **drop, 'Run': n, 'Item name': drop.get('item_name', drop.get('input', '')), 'Extra input': drop.get('extra', '') } for n, drop_list in drops.items() for drop in drop_list] def select_drops_from_filters(event=None): tree.delete(*tree.get_children()) # The filtering function breaks if column name has underscore in it - potential issue that could be fixed.. all_filter = lambda x: all( str(x.get(f.split('_')[-1], '')) == getattr(self, f).get() or getattr(self, f).get() == '' for f in filters) for drop in flat_drops: if all_filter(drop): if drop.get('Grailer', False) == 'True': tree.insert('', tk.END, values=[drop.get(col, '') for col in cols], tag='Grail') elif drop.get('Eth Grailer', False) == 'True': tree.insert('', tk.END, values=[drop.get(col, '') for col in cols], tag='EthGrail') else: tree.insert('', tk.END, values=[drop.get(col, '') for col in cols]) drop_table_fr = tkd.Frame(self.tabcontrol) self.tabcontrol.add(drop_table_fr, text='Drop table') cols = [ "Run", "Item name", "Extra input", "Real time", "TC", "QLVL", "Item Class", "Grailer", "Eth Grailer", "Session", "Rarity" ] tree_frame = tkd.Frame(drop_table_fr) btn_frame2 = tkd.Frame(drop_table_fr) btn_frame2.pack(side=tk.BOTTOM) vscroll_tree = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL) hscroll_tree = ttk.Scrollbar(drop_table_fr, orient=tk.HORIZONTAL) tree = tkd.Treeview(tree_frame, selectmode=tk.BROWSE, yscrollcommand=vscroll_tree.set, xscrollcommand=hscroll_tree.set, show='headings', columns=cols, alternate_colour=True) hscroll_tree.config(command=tree.xview) vscroll_tree.config(command=tree.yview) tkd.Button(btn_frame2, text='Save as .csv', command=lambda: self.save_to_csv(tree)).pack(side=tk.LEFT, fill=tk.X) combofr = tkd.Frame(tree_frame) vscroll_tree.pack(side=tk.RIGHT, fill=tk.Y) combofr.pack(fill=tk.X) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) hscroll_tree.pack(side=tk.BOTTOM, fill=tk.X) tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) tree['columns'] = cols widths = [35, 200, 140, 120, 35, 38, 100, 47, 65, 80, 80] cb_width = [3, 30, 20, 16, 3, 3, 13, 5, 7, 10, 10] tree.tag_configure('Grail', background='#e6ffe6') tree.tag_configure('EthGrail', background='light goldenrod yellow') filters = [] for i, col in enumerate(cols): tree.column(col, stretch=tk.NO, minwidth=0, width=widths[i]) if col in ['Run', 'TC', 'QLVL']: sort_by = 'num' sort_key = lambda x: float('-inf') if x == '' else float( x.replace('%', '')) else: sort_by = 'name' sort_key = lambda x: x tree.heading(col, text=col, sort_by=sort_by) name = 'combofilter_' + col filters.append(name) setattr( self, name, tkd.Combobox(combofr, values=sorted(set( str(x.get(col, '')) for x in flat_drops).union({''}), key=sort_key), state="readonly", width=cb_width[i])) getattr(self, name).pack(side=tk.LEFT) getattr(self, name).bind('<<ComboboxSelected>>', select_drops_from_filters) select_drops_from_filters()
def __init__(self): # Check if application is already open self.title = 'MF run counter' # Create error logger lh = logging.FileHandler(filename='mf_timer.log', mode='w', delay=True) logging.basicConfig(handlers=[lh], format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%H:%M:%S', level=logging.WARNING) # Check OS self.os_platform = platform.system() self.os_release = platform.release() if not self.os_platform == 'Windows': raise SystemError("MF Run Counter only supports windows") # Create root self.root = tkd.Tk() # Ensure errors are handled with an exception pop-up if encountered self.root.report_callback_exception = self.report_callback_exception # Build/load config file self.cfg = self.load_config_file() if hasattr(logging, self.cfg['DEFAULT']['logging_level']): logging.getLogger().setLevel(getattr(logging, self.cfg['DEFAULT']['logging_level'])) self.SP_game_path = self.cfg['DEFAULT']['SP_game_path'] self.MP_game_path = self.cfg['DEFAULT']['MP_game_path'] self.herokuapp_username = self.cfg['DEFAULT']['herokuapp_username'] self.herokuapp_password = base64.b64decode(self.cfg['DEFAULT']['herokuapp_password']).decode('utf-8') self.webproxies = other_utils.safe_eval(self.cfg['DEFAULT']['webproxies']) self.automode = other_utils.safe_eval(self.cfg['AUTOMODE']['automode']) self.end_run_in_menu = other_utils.safe_eval(self.cfg['AUTOMODE']['end_run_in_menu']) self.pause_on_esc_menu = other_utils.safe_eval(self.cfg['AUTOMODE']['pause_on_esc_menu']) self.always_on_top = other_utils.safe_eval(self.cfg['OPTIONS']['always_on_top']) self.tab_switch_keys_global = other_utils.safe_eval(self.cfg['OPTIONS']['tab_switch_keys_global']) self.check_for_new_version = other_utils.safe_eval(self.cfg['OPTIONS']['check_for_new_version']) self.enable_sound_effects = other_utils.safe_eval(self.cfg['OPTIONS']['enable_sound_effects']) self.start_run_delay_seconds = other_utils.safe_eval(self.cfg['OPTIONS']['start_run_delay_seconds']) self.show_drops_tab_below = other_utils.safe_eval(self.cfg['OPTIONS']['show_drops_tab_below']) self.active_theme = self.cfg['OPTIONS']['active_theme'].lower() self.auto_upload_herokuapp = other_utils.safe_eval(self.cfg['OPTIONS']['auto_upload_herokuapp']) self.auto_archive_hours = other_utils.safe_eval(self.cfg['OPTIONS']['auto_archive_hours']) self.autocompletion_unids = other_utils.safe_eval(self.cfg['OPTIONS']['autocompletion_unids']) self.add_to_last_run = other_utils.safe_eval(self.cfg['OPTIONS']['add_to_last_run']) self.disable_scaling = other_utils.safe_eval(self.cfg['OPTIONS']['disable_dpi_scaling']) # UI config self.show_buttons = other_utils.safe_eval(self.cfg['UI']['show_buttons']) self.show_drops_section = other_utils.safe_eval(self.cfg['UI']['show_drops_section']) self.show_advanced_tracker = other_utils.safe_eval(self.cfg['UI']['show_advanced_tracker']) self.show_xp_tracker = other_utils.safe_eval(self.cfg['UI']['show_xp_tracker']) # Initiate variables for memory reading self.is_user_admin = reader_utils.is_user_admin() self.advanced_error_thrown = False self.d2_reader = None # Load theme if self.active_theme not in available_themes: self.active_theme = 'vista' self.theme = Theme(used_theme=self.active_theme) # Create hotkey queue and initiate process for monitoring the queue self.queue = queue.Queue(maxsize=1) self.process_queue() # Check for version update if self.check_for_new_version: self.dl_count = github_releases.check_newest_version(release_repo) else: self.dl_count = '' # Load profile info self.make_profile_folder() self.profiles = [x[:-5] for x in os.listdir('Profiles') if x.endswith('.json') and not x == 'grail.json'] self.active_profile = self.cfg['DEFAULT']['active_profile'] if len(self.profiles) == 0: self.active_profile = '' elif len(self.profiles) > 0 and self.active_profile not in self.profiles: self.active_profile = self.profiles[0] self.profiles = self.sorted_profiles() # Modify root window self.root.title(self.title) self.clickable = True self.root.resizable(False, False) self.root.geometry('+%d+%d' % other_utils.safe_eval(self.cfg['DEFAULT']['window_start_position'])) self.root.config(borderwidth=2, height=365, width=240, relief='raised') # self.root.wm_attributes("-transparentcolor", "purple") self.root.wm_attributes("-topmost", self.always_on_top) self.root.focus_get() self.root.protocol("WM_DELETE_WINDOW", self.Quit) self.root.iconbitmap(media_path + 'icon.ico') self.root.pack_propagate(False) # Build banner image and make window draggable on the banner d2banner = media_path + 'd2icon.png' img = tk.PhotoImage(file=d2banner) self.img_panel = tkd.Label(self.root, image=img, borderwidth=0) self.img_panel.pack() self.img_panel.bind("<ButtonPress-1>", self.root.start_move) self.img_panel.bind("<ButtonRelease-1>", self.root.stop_move) self.img_panel.bind("<B1-Motion>", self.root.on_motion) self.root.bind("<Delete>", self.delete_selection) self.root.bind("<Left>", self.root.moveleft) self.root.bind("<Right>", self.root.moveright) self.root.bind("<Up>", self.root.moveup) self.root.bind("<Down>", self.root.movedown) # Add buttons to main widget self.btn_frame = tkd.Frame(self.root) tkd.Button(self.btn_frame, text='Delete selection', command=self.delete_selection).pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=[2, 1], pady=1) tkd.Button(self.btn_frame, text='Archive session', command=self.ArchiveReset).pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=[0, 1], pady=1) # Build tabs self.caret_frame = tkd.Frame(self.root) self.drops_frame = tkd.Frame(self.caret_frame) self.adv_stats_frame = tkd.Frame(self.caret_frame) self.tabcontrol = tkd.Notebook(self.root) self.tabcontrol.pack(expand=False, fill=tk.BOTH) self.profile_tab = Profile(self, parent=self.tabcontrol) self.timer_tab = MFRunTimer(self, parent=self.tabcontrol) self.drops_tab = Drops(self, parent=self.drops_frame) self.options_tab = Options(self, self.timer_tab, self.drops_tab, parent=self.tabcontrol) self.grail_tab = Grail(self, parent=self.tabcontrol) self.about_tab = About(self, parent=self.tabcontrol) self.tabcontrol.add(self.timer_tab, text='Timer') self.tabcontrol.add(self.options_tab, text='Options') self.tabcontrol.add(self.profile_tab, text='Profile') self.tabcontrol.add(self.grail_tab, text='Grail') self.tabcontrol.add(self.about_tab, text='About') self.root.bind("<<NotebookTabChanged>>", lambda _e: self.notebook_tab_change()) self.profile_tab.update_descriptive_statistics() self.toggle_drops_frame(show=self.show_drops_tab_below) self.drops_caret = tkd.CaretButton(self.drops_frame, active=self.show_drops_tab_below, command=self.toggle_drops_frame, text='Drops', compound=tk.RIGHT, height=13) self.drops_caret.propagate(False) self.drops_caret.pack(side=tk.BOTTOM, fill=tk.X, expand=True, padx=[2, 1], pady=[0, 1]) tracker_is_active = other_utils.safe_eval(self.cfg['AUTOMODE']['advanced_tracker_open']) and self.automode == 2 and self.is_user_admin self.advanced_stats_tracker = StatsTracker(self, self.adv_stats_frame) self.advanced_stats_caret = tkd.CaretButton(self.adv_stats_frame, active=tracker_is_active, text='Advanced stats', compound=tk.RIGHT, height=13, command=self.toggle_advanced_stats_frame) self.advanced_stats_caret.propagate(False) self.advanced_stats_caret.pack(side=tk.BOTTOM, fill=tk.X, expand=True, padx=[2, 1], pady=[0, 1]) # Register binds for changing tabs if self.tab_switch_keys_global: self.options_tab.tab2.hk.register(['control', 'shift', 'next'], callback=lambda event: self.queue.put(self.tabcontrol.next_tab)) self.options_tab.tab2.hk.register(['control', 'shift', 'prior'], callback=lambda event: self.queue.put(self.tabcontrol.prev_tab)) else: self.root.bind_all('<Control-Shift-Next>', lambda event: self.tabcontrol.next_tab()) self.root.bind_all('<Control-Shift-Prior>', lambda event: self.tabcontrol.prev_tab()) # Load save state and start autosave process active_state = self.load_state_file() self.LoadActiveState(active_state) self.root.after(30000, self._autosave_state) # Apply styling options self.theme.apply_theme_style() self.theme.update_colors() # Automode and advanced stats loop self.am_lab = tk.Text(self.root, height=1, width=13, wrap=tk.NONE, bg="black", font=('Segoe UI', 9), cursor='', borderwidth=0) self.am_lab.tag_configure("am", foreground="white", background="black") self.am_lab.tag_configure("on", foreground="lawn green", background="black") self.am_lab.tag_configure("off", foreground="red", background="black") self.am_lab.place(x=1, y=0.4) self.toggle_automode() self.toggle_advanced_stats_frame(show=tracker_is_active) # A trick to disable windows DPI scaling - the app doesnt work well with scaling, unfortunately if self.os_release == '10' and self.disable_scaling: ctypes.windll.shcore.SetProcessDpiAwareness(2) # Used if "auto archive session" is activated self.profile_tab.auto_reset_session() # Pressing ALT_L paused UI updates when in focus, disable (probably hooked to opening menus) self.root.unbind_all('<Alt_L>') # Start the program self.root.mainloop()
def open_archive_browser(self): chosen = self.archive_dropdown.get() if chosen == '': # If nothing is selected the function returns return # We build the new tkinter window to be opened new_win = tkd.Toplevel() new_win.title('Archive browser') new_win.wm_attributes('-topmost', 1) disp_coords = tk_utils.get_displaced_geom(self.main_frame.root, 400, 460) new_win.geometry(disp_coords) new_win.focus_get() new_win.iconbitmap(os.path.join(getattr(sys, '_MEIPASS', os.path.abspath('.')), media_path + 'icon.ico')) new_win.minsize(400, 460) title = tkd.Label(new_win, text='Archive browser', font='Helvetica 14') # Handle how loading of session data should be treated in the 3 different cases if chosen == 'Active session': # Load directly from timer module session_time = self.main_frame.timer_tab.session_time laps = self.main_frame.timer_tab.laps drops = self.main_frame.drops_tab.drops elif chosen == 'Profile history': # Load everything from profile .json, and append data from timer module active = self.main_frame.load_state_file() laps = [] session_time = 0 drops = dict() # Concatenate information from each available session for key in [x for x in active.keys() if x not in ['active_state', 'extra_data']]: session_drops = active[key].get('drops', dict()) for run_no, run_drop in session_drops.items(): drops[str(int(run_no)+len(laps))] = run_drop laps.extend(active[key].get('laps', [])) session_time += active[key].get('session_time', 0) # Append data for active session from timer module for run_no, run_drop in self.main_frame.drops_tab.drops.items(): drops[str(int(run_no) + len(laps))] = run_drop laps.extend(self.main_frame.timer_tab.laps) session_time += self.main_frame.timer_tab.session_time else: # Load selected session data from profile .json active = self.main_frame.load_state_file() chosen_archive = active.get(chosen, dict()) session_time = chosen_archive.get('session_time', 0) laps = chosen_archive.get('laps', []) drops = chosen_archive.get('drops', dict()) # Ensure no division by zero errors by defaulting to displaying 0 avg_lap = sum(laps) / len(laps) if laps else 0 pct = sum(laps) * 100 / session_time if session_time > 0 else 0 # Configure the list frame with scrollbars which displays the archive of the chosen session list_win = tkd.Frame(new_win) list_frame = tkd.Frame(list_win) vscroll = ttk.Scrollbar(list_frame, orient=tk.VERTICAL) hscroll = ttk.Scrollbar(list_win, orient=tk.HORIZONTAL) txt_list = tkd.Text(list_frame, yscrollcommand=vscroll.set, xscrollcommand=hscroll.set, font='courier 10', wrap=tk.WORD, state=tk.NORMAL, cursor='', exportselection=1, name='archivebrowser') # txt_list.bind('<FocusOut>', lambda e: txt_list.tag_remove(tk.SEL, "1.0", tk.END)) # Lose selection when shifting focus vscroll.pack(side=tk.RIGHT, fill=tk.Y) txt_list.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) txt_list.tag_configure("HEADER", font=tkFont(family='courier', size=12, weight='bold', underline=True)) hscroll.config(command=txt_list.xview) vscroll.config(command=txt_list.yview) # Build header for output file with information and descriptive statistics output = [['Statistics'], ['Character name: ', self.extra_data.get('Character name', '')], ['Run type: ', self.extra_data.get('Run type', '')], ['Game mode: ', self.extra_data.get('Game mode', 'Single Player')], [''], ['Total session time: ', utils.other_utils.build_time_str(session_time)], ['Total run time: ', utils.other_utils.build_time_str(sum(laps))], ['Average run time: ', utils.other_utils.build_time_str(avg_lap)], ['Fastest run time: ', utils.other_utils.build_time_str(min(laps, default=0))], ['Number of runs: ', str(len(laps))], ['Time spent in runs: ', str(round(pct, 2)) + '%'], ['']] # Backwards compatibility with old drop format for k, v in drops.items(): for i in range(len(v)): if not isinstance(v[i], dict): drops[k][i] = {'item_name': None, 'input': v[i], 'extra': ''} # List all drops collected if drops: if any(drop for drop in drops.values()): output.append(['Collected drops']) for run_no, drop in drops.items(): if drop: str_n = ' ' * max(len(str(len(laps))) - len(str(run_no)), 0) + str(run_no) output.append(['Run ' + str_n, '', *[x['input'] for x in drop]]) output.append(['']) if laps: output.append(['Run times']) # Loop through all runs and add run times and drops for each run for n, lap in enumerate(laps, 1): str_n = ' ' * max(len(str(len(laps))) - len(str(n)), 0) + str(n) droplst = drops.get(str(n), []) tmp = ['Run ' + str_n + ': ', utils.other_utils.build_time_str(lap)] if droplst: tmp += [d['input'] for d in droplst] output.append(tmp) # Format string list to be shown in the archive browser for i, op in enumerate(output, 1): tmpstr = ''.join(op[:2]) if len(op) > 2: tmpstr += ' --- ' + ', '.join(op[2:]) if txt_list.get('1.0', tk.END) != '\n': tmpstr = '\n' + tmpstr txt_list.insert(tk.END, tmpstr) if op[0] in ['Statistics', 'Collected drops', 'Run times']: txt_list.tag_add("HEADER", str(i) + ".0", str(i) + ".0 lineend") # Add bold tags # txt_list.tag_add("BOLD", "1.0", "1.15") # txt_list.tag_add("BOLD", "2.0", "2.9") # txt_list.tag_add("BOLD", "3.0", "3.10") # txt_list.tag_add("BOLD", "5.0", "5.19") # txt_list.tag_add("BOLD", "6.0", "6.15") # txt_list.tag_add("BOLD", "7.0", "7.17") # txt_list.tag_add("BOLD", "8.0", "8.17") # txt_list.tag_add("BOLD", "9.0", "9.15") # txt_list.tag_add("BOLD", "10.0", "10.19") # txt_list.tag_add("BOLD", "1.16", "1.0 lineend") # txt_list.tag_add("BOLD", "2.16", "2.0 lineend") # txt_list.tag_add("BOLD", "3.16", "3.0 lineend") # txt_list.tag_add("BOLD", "5.20", "5.0 lineend") # txt_list.tag_add("BOLD", "6.20", "6.0 lineend") # txt_list.tag_add("BOLD", "7.20", "7.0 lineend") # txt_list.tag_add("BOLD", "8.20", "8.0 lineend") # txt_list.tag_add("BOLD", "9.20", "9.0 lineend") # txt_list.tag_add("BOLD", "10.20", "10.0 lineend") txt_list.tag_add("HEADER", "12.0", "12.0 lineend") txt_list.config(state=tk.DISABLED) button_frame = tkd.Frame(new_win) tkd.Button(button_frame, text='Copy to clipboard', command=lambda: self.copy_to_clipboard(new_win, txt_list.get(1.0, tk.END))).pack(side=tk.LEFT, fill=tk.X) tkd.Button(button_frame, text='Save as .txt', command=lambda: self.save_to_txt(txt_list.get(1.0, tk.END))).pack(side=tk.LEFT, fill=tk.X) tkd.Button(button_frame, text='Save as .csv', command=lambda: self.save_to_csv(output)).pack(side=tk.LEFT, fill=tk.X) # Packs all the buttons and UI in the archive browser. Packing order is very important: # TOP: Title first (furthest up), then list frame # BOTTOM: Buttons first (furthest down) and then horizontal scrollbar title.pack(side=tk.TOP) list_win.pack(side=tk.TOP, fill=tk.BOTH, expand=1) list_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=1) hscroll.pack(side=tk.BOTTOM, fill=tk.X) button_frame.pack(side=tk.BOTTOM) theme = Theme(self.main_frame.active_theme) theme.update_colors()
def make_widgets(self): self.gamemode_frame = tkd.LabelFrame(self, height=LAB_HEIGHT, width=LAB_WIDTH) self.gamemode_frame.propagate(False) self.gamemode_lab = tkd.Label( self.gamemode_frame, text='Game mode', tooltip= 'If Multiplayer is selected, the .map file is used to check for updates.\n' 'Thus, new runs begin every time you enter a new game (since your local .map files will be updated by this)\n\n' 'If Single Player is selected the .d2s file is used to check for updates.\n' 'Thus, a new run begins every time you leave a game (since your .d2s files are saved upon exit)' ) self.game_mode = tk.StringVar() self.game_mode.set(self.main_frame.profile_tab.game_mode.get()) self.gamemode_cb = ttk.Combobox( self.gamemode_frame, textvariable=self.game_mode, state='readonly', values=['Single Player', 'Multiplayer']) self.gamemode_cb.bind("<FocusOut>", lambda e: self.gamemode_cb.selection_clear()) self.gamemode_cb.config(width=11) self.game_mode.trace_add( 'write', lambda name, index, mode: self.update_game_mode()) self.charname_frame = tkd.LabelFrame(self, height=LAB_HEIGHT, width=LAB_WIDTH) self.charname_frame.propagate(False) self.char_var = tk.StringVar() self.char_var.set(self.main_frame.profile_tab.char_name.get()) self.charname_text_lab = tkd.Label( self.charname_frame, text='Character name', tooltip='Your character name is inferred from the active profile.\n' 'Make sure the character name in your profile is matching your in-game character name' ) self.charname_val_lab = tkd.Label(self.charname_frame, textvariable=self.char_var) self.sp_path_lab = tkd.Label(self, text='Game path (Single Player)') self.SP_game_path = tk.StringVar() self.SP_game_path.set(self.main_frame.SP_game_path) self.sp_path_entry = tkd.Entry(self, textvariable=self.SP_game_path) self.sp_path_frame = tkd.Frame(self) self.sp_path_get = tkd.Button( self.sp_path_frame, text='Get', command=lambda: self.get_game_path(is_sp=True), tooltip= 'The app tries to automatically find your game path for single player\n' 'If nothing is returned you have to type it in manually') self.sp_path_apply = tkd.Button( self.sp_path_frame, text='Apply', command=self.apply_path_ch, tooltip='Apply the current specified path') self.mp_path_lab = tkd.Label(self, text='Game path (Multiplayer)') self.MP_game_path = tk.StringVar() self.MP_game_path.set(self.main_frame.MP_game_path) self.mp_path_entry = tkd.Entry(self, textvariable=self.MP_game_path) self.mp_path_frame = tkd.Frame(self) self.mp_path_get = tkd.Button( self.mp_path_frame, text='Get', command=lambda: self.get_game_path(is_sp=False), tooltip= 'The app tries to automatically find your game path for multiplayer\n' 'If nothing is returned you have to type it in manually') self.mp_path_apply = tkd.Button( self.mp_path_frame, text='Apply', command=self.apply_path_ch, tooltip='Apply the current specified path') # Stuff for advanced mode self.advanced_mode_stop = self.add_flag( flag_name='Stop when leaving', comment= 'On: Stops the current run when you exit to menu.\nOff: The run counter will continue ticking until you enter a new game', pack=False, config_section='AUTOMODE') self.advanced_pause_on_esc_menu = self.add_flag( flag_name='Pause on ESC menu', comment= 'When activated, the counter will be paused when ESC menu\nis open inside d2 (not working for 1.14b and 1.14c)', pack=False, config_section='AUTOMODE') self.advanced_automode_warning = tkd.Label( self, text='"Advanced automode" is highly \n' 'discouraged when playing\n' 'multiplayer, as it might result\n' 'in a ban.\n' 'Explanation: Advanced automode\n' 'utilizes "memory reading" of the\n' 'D2 process to discover information\n' 'about the current game state,\n' 'and this could be deemed cheating.', justify=tk.LEFT) self.toggle_automode_btn(first=True)