def body(self, master): master.columnconfigure(0, weight=1) master.columnconfigure(1, weight=0) master.rowconfigure(0, weight=1) scrlbar_v = ttk.Scrollbar(master, orient=tk.VERTICAL) scrlbar_h = ttk.Scrollbar(master, orient=tk.HORIZONTAL) txtbox = TtkText(master, ttk.Style(), wrap=tk.WORD, yscrollcommand=scrlbar_v.set, xscrollcommand=scrlbar_h.set) scrlbar_v.config(command=txtbox.yview) scrlbar_h.config(command=txtbox.xview) txtbox.insert(tk.END, CNST.WELCOME) txtbox.config(state=tk.DISABLED) txtbox.grid(column=0, row=0, sticky="news") scrlbar_v.grid(column=1, row=0, sticky="sn") scrlbar_h.grid(column=0, row=1, pady=(0, 3), sticky="ew") btnframe = ttk.Frame(master) btnframe.columnconfigure((0, 1), weight=1) btconfirm = ttk.Button(btnframe, text="OK", command=lambda: self.done(DIAGSIG.SUCCESS)) btcancel = ttk.Button(btnframe, text="Exit", command=lambda: self.done(DIAGSIG.FAILURE)) btconfirm.grid(column=0, row=2, padx=(0, 3), sticky="ew") btcancel.grid(column=1, row=2, padx=(3, 0), sticky="ew") btnframe.grid(column=0, row=2, columnspan=2, sticky="ew")
def body(self, master): self.protocol("WM_DELETE_WINDOW", self.destroy) try: # Try block should only catch from the os.listdir call directly below. jsonfiles = [ i for i in os.listdir(self.demodir) if os.path.splitext(i)[1] == ".json" ] # Add json files to self.todel if their demo files are in todel choppedfilestodel = [ os.path.splitext(i)[0] for i in self.filestodel ] choppedfiles = None for i, j in enumerate(jsonfiles): if os.path.splitext(j)[0] in choppedfilestodel: self.filestodel.append(jsonfiles[i]) if self.deluselessjson: # also delete orphaned files choppedfilestodel = None choppedfiles = {os.path.splitext(i)[0] for i in self.files} for i, j in enumerate(jsonfiles): if not os.path.splitext(j)[0] in choppedfiles: self.filestodel.append(jsonfiles[i]) self.startmsg = "This operation will delete the following file(s):\n\n" + \ "\n".join(self.filestodel) except (OSError, PermissionError, FileNotFoundError) as error: self.startmsg = f"Error getting JSON files: !\n{type(error).__name__}: {error}\n\n" \ f"This operation will delete the following file(s):\n\n" + "\n".join(self.filestodel) self.okbutton = ttk.Button(master, text="Delete!", command=lambda: self.confirm(1)) self.cancelbutton = ttk.Button(master, text="Cancel", command=self.destroy) self.closebutton = ttk.Button(master, text="Close", command=self.destroy) self.canceloperationbutton = ttk.Button(master, text="Abort", command=self._stopoperation) textframe = ttk.Frame(master, padding=(5, 5, 5, 5)) textframe.grid_columnconfigure(0, weight=1) textframe.grid_rowconfigure(0, weight=1) self.textbox = TtkText(textframe, self.styleobj, wrap=tk.NONE, width=40, height=20) self.vbar = ttk.Scrollbar(textframe, orient=tk.VERTICAL, command=self.textbox.yview) self.vbar.grid(column=1, row=0, sticky="ns") self.hbar = ttk.Scrollbar(textframe, orient=tk.HORIZONTAL, command=self.textbox.xview) self.hbar.grid(column=0, row=1, sticky="ew") self.textbox.config(xscrollcommand=self.hbar.set, yscrollcommand=self.vbar.set) self.textbox.grid(column=0, row=0, sticky="news", padx=(0, 3), pady=(0, 3)) self.textbox.delete("0.0", tk.END) self.textbox.insert(tk.END, self.startmsg) self.textbox.config(state=tk.DISABLED) textframe.pack(fill=tk.BOTH, expand=1) self.okbutton.pack(side=tk.LEFT, fill=tk.X, expand=1, padx=(0, 3)) self.cancelbutton.pack(side=tk.LEFT, fill=tk.X, expand=1, padx=(3, 0))
class Deleter(BaseDialog): """ A dialog that constructs deletion requests for demos and prompts the user whether they are sure of the deletion. If yes was selected, starts a stoppable thread and waits for its completion. After the dialog is closed: `self.result.state` will be SUCCESS if the thread was started, else FAILURE. `self.result.data` will be None if the thread was not started, else the thread's termination signal. """ def __init__(self, parent, demodir, files, selected, evtblocksz, deluselessjson, styleobj, eventfileupdate="passive"): """ parent: Parent widget, should be a `Tk` or `Toplevel` instance. demodir: Absolute path to the directory containing the demos. (str) files: List of files that shall be included in the deletion evaluation. selected: List of boolean values so that files[n] will be deleted if selected[n] == True evtblocksz: Size of blocks (in bytes) to read _events.txt in. deluselessjson: Boolean value that instructs the deletion evaluation to include json files with no demos. styleobj: Instance of tkinter.ttk.Style. eventfileupdate: Has two valid forms: "selectivemove" and "passive" (default). "selectivemove" only writes Logchunks from the old eventfile to the new one if it can be assigned to a file in a previously declared list of acceptable files. !!!This mode requires every demo of demodir to be present in files!!! "passive" moves all Logchunks from the old eventfile to the new one unless their demo was explicitly deleted. """ super().__init__(parent, "Delete...") self.master = parent self.demodir = demodir self.files = files self.selected = selected self.evtblocksz = evtblocksz self.deluselessjson = deluselessjson self.styleobj = styleobj self.eventfileupdate = eventfileupdate self.filestodel = [ j for i, j in enumerate(self.files) if self.selected[i] ] self.threadgroup = ThreadGroup(ThreadDelete, self.master) self.threadgroup.decorate_and_patch(self, self._after_callback) def body(self, master): self.protocol("WM_DELETE_WINDOW", self.destroy) try: # Try block should only catch from the os.listdir call directly below. jsonfiles = [ i for i in os.listdir(self.demodir) if os.path.splitext(i)[1] == ".json" ] # Add json files to self.todel if their demo files are in todel choppedfilestodel = [ os.path.splitext(i)[0] for i in self.filestodel ] choppedfiles = None for i, j in enumerate(jsonfiles): if os.path.splitext(j)[0] in choppedfilestodel: self.filestodel.append(jsonfiles[i]) if self.deluselessjson: # also delete orphaned files choppedfilestodel = None choppedfiles = {os.path.splitext(i)[0] for i in self.files} for i, j in enumerate(jsonfiles): if not os.path.splitext(j)[0] in choppedfiles: self.filestodel.append(jsonfiles[i]) self.startmsg = "This operation will delete the following file(s):\n\n" + \ "\n".join(self.filestodel) except (OSError, PermissionError, FileNotFoundError) as error: self.startmsg = f"Error getting JSON files: !\n{type(error).__name__}: {error}\n\n" \ f"This operation will delete the following file(s):\n\n" + "\n".join(self.filestodel) self.okbutton = ttk.Button(master, text="Delete!", command=lambda: self.confirm(1)) self.cancelbutton = ttk.Button(master, text="Cancel", command=self.destroy) self.closebutton = ttk.Button(master, text="Close", command=self.destroy) self.canceloperationbutton = ttk.Button(master, text="Abort", command=self._stopoperation) textframe = ttk.Frame(master, padding=(5, 5, 5, 5)) textframe.grid_columnconfigure(0, weight=1) textframe.grid_rowconfigure(0, weight=1) self.textbox = TtkText(textframe, self.styleobj, wrap=tk.NONE, width=40, height=20) self.vbar = ttk.Scrollbar(textframe, orient=tk.VERTICAL, command=self.textbox.yview) self.vbar.grid(column=1, row=0, sticky="ns") self.hbar = ttk.Scrollbar(textframe, orient=tk.HORIZONTAL, command=self.textbox.xview) self.hbar.grid(column=0, row=1, sticky="ew") self.textbox.config(xscrollcommand=self.hbar.set, yscrollcommand=self.vbar.set) self.textbox.grid(column=0, row=0, sticky="news", padx=(0, 3), pady=(0, 3)) self.textbox.delete("0.0", tk.END) self.textbox.insert(tk.END, self.startmsg) self.textbox.config(state=tk.DISABLED) textframe.pack(fill=tk.BOTH, expand=1) self.okbutton.pack(side=tk.LEFT, fill=tk.X, expand=1, padx=(0, 3)) self.cancelbutton.pack(side=tk.LEFT, fill=tk.X, expand=1, padx=(3, 0)) def confirm(self, param): if param == 1: self.okbutton.pack_forget() self.cancelbutton.pack_forget() self.canceloperationbutton.pack(side=tk.LEFT, fill=tk.X, expand=1) self._startthread() def _stopoperation(self): self.threadgroup.join_thread() def _startthread(self): self.threadgroup.start_thread( demodir=self.demodir, files=self.files, selected=self.selected, filestodel=self.filestodel, evtblocksz=self.evtblocksz, eventfileupdate=self.eventfileupdate, ) def _after_callback(self, queue_elem): """ Gets stuff from self.queue_out that the thread writes to, then modifies UI based on queue elements. (Additional decoration in __init__) """ if queue_elem[0] == THREADSIG.INFO_CONSOLE: self.appendtextbox(queue_elem[1]) return THREADGROUPSIG.CONTINUE elif queue_elem[0] < 0x100: # Finish self.result.state = DIAGSIG.SUCCESS self.result.data = queue_elem[0] self.canceloperationbutton.pack_forget() self.closebutton.pack(side=tk.LEFT, fill=tk.X, expand=1) return THREADGROUPSIG.FINISHED def appendtextbox(self, _inp): with self.textbox: self.textbox.insert(tk.END, str(_inp)) self.textbox.yview_moveto(1.0) self.textbox.update() def destroy(self): self._stopoperation() super().destroy()
def body(self, master): """UI""" self.protocol("WM_DELETE_WINDOW", self.done) master.grid_columnconfigure((0, 1), weight=1) dir_sel_lblfrm = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label( master, "Paths")) dir_sel_lblfrm.grid_columnconfigure(0, weight=1) ds_widgetframe = ttk.Frame(dir_sel_lblfrm, style="Contained.TFrame") ds_widgetframe.grid_columnconfigure(1, weight=1) for i, j in enumerate(( ("Steam:", self.steamdir_var), ("HLAE:", self.hlaedir_var), )): dir_label = ttk.Label(ds_widgetframe, style="Contained.TLabel", text=j[0]) dir_entry = ttk.Entry(ds_widgetframe, state="readonly", textvariable=j[1]) def tmp_handler(self=self, var=j[1]): return self._sel_dir(var) dir_btn = ttk.Button(ds_widgetframe, style="Contained.TButton", command=tmp_handler, text="Change path...") dir_label.grid(row=i, column=0) dir_entry.grid(row=i, column=1, sticky="ew") dir_btn.grid(row=i, column=2, padx=(3, 0)) self.error_steamdir_invalid = ttk.Label( dir_sel_lblfrm, anchor = tk.N, justify = tk.CENTER, style = "Error.Contained.TLabel", text = "Getting steam users failed! Please select the root folder called " \ "\"Steam\".\nEventually check for permission conflicts." ) self.warning_steamdir_mislocated = ttk.Label( dir_sel_lblfrm, anchor=tk.N, style="Warning.Contained.TLabel", text= "The queried demo and the Steam directory are on seperate drives.") userselectframe = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label( master, "Select user profile if needed")) userselectframe.grid_columnconfigure(0, weight=1) self.info_launchoptions_not_found = ttk.Label( userselectframe, anchor=tk.N, style="Info.Contained.TLabel", text="Launch configuration not found, it likely does not exist.") self.userselectbox = ttk.Combobox(userselectframe, textvariable=self.userselectvar, state="readonly") # Once changed, observer callback triggered by self.userselectvar launchoptionsframe = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label( master, "Launch options")) launchoptionsframe.grid_columnconfigure(0, weight=1) launchoptwidgetframe = ttk.Frame(launchoptionsframe, borderwidth=4, relief=tk.RAISED, padding=(5, 4, 5, 4)) launchoptwidgetframe.grid_columnconfigure((1, ), weight=10) self.head_args_lbl = ttk.Label(launchoptwidgetframe, text="[...]/hl2.exe -steam -game tf") self.launchoptionsentry = ttk.Entry(launchoptwidgetframe, textvariable=self.launchoptionsvar) pluslabel = ttk.Label(launchoptwidgetframe, text="+") self.demo_play_arg_entry = ttk.Entry(launchoptwidgetframe, state="readonly", textvariable=self.playdemoarg) self.end_q_mark_label = ttk.Label(launchoptwidgetframe, text="") #launchoptwidgetframe.grid_propagate(False) self.use_hlae_checkbox = ttk.Checkbutton( launchoptionsframe, variable=self.usehlae_var, text="Launch using HLAE", style="Contained.TCheckbutton", command=self._toggle_hlae_cb) self.warning_not_in_tf_dir = ttk.Label( launchoptionsframe, anchor=tk.N, style="Warning.Contained.TLabel", text= "The demo can not be played as it is not in Team Fortress' file system (/tf)" ) # self.demo_play_arg_entry.config(width = len(self.playdemoarg.get()) + 2) rconlabelframe = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label(master, "RCON")) rconlabelframe.grid_columnconfigure(1, weight=1) self.rcon_btn = ttk.Button(rconlabelframe, text="Send command", command=self._rcon, width=15, style="Centered.TButton") self.rcon_btn.grid(row=0, column=0) self.rcon_txt = TtkText(rconlabelframe, self._style, height=4, width=48, wrap=tk.CHAR) rcon_scrollbar = ttk.Scrollbar(rconlabelframe, orient=tk.HORIZONTAL, command=self.rcon_txt.xview) self.rcon_txt.insert(tk.END, "Status: [.]\n\n\n") self.rcon_txt.mark_set("spinner", "1.9") self.rcon_txt.mark_gravity("spinner", tk.LEFT) self.rcon_txt.configure(xscrollcommand=rcon_scrollbar.set, state=tk.DISABLED) self.rcon_txt.grid(row=0, column=1, sticky="news", padx=(5, 0)) rcon_scrollbar.grid(row=1, column=1, sticky="ew", padx=(5, 0)) # grid start # dir selection widgets are already gridded ds_widgetframe.grid(sticky="news") self.error_steamdir_invalid.grid() self.warning_steamdir_mislocated.grid() dir_sel_lblfrm.grid(columnspan=2, pady=5, sticky="news") self.userselectbox.grid(sticky="we") self.info_launchoptions_not_found.grid() userselectframe.grid(columnspan=2, pady=5, sticky="news") self.head_args_lbl.grid(row=0, column=0, columnspan=3, sticky="news") self.launchoptionsentry.grid(row=1, column=0, columnspan=3, sticky="news") pluslabel.grid(row=2, column=0) self.demo_play_arg_entry.grid(row=2, column=1, sticky="news") self.end_q_mark_label.grid(row=2, column=2) launchoptwidgetframe.grid(sticky="news") self.use_hlae_checkbox.grid(sticky="w", pady=(5, 0), ipadx=4) self.warning_not_in_tf_dir.grid() launchoptionsframe.grid(columnspan=2, pady=(5, 10), sticky="news") rconlabelframe.grid(columnspan=2, pady=(5, 10), sticky="news") self.btconfirm = ttk.Button(master, text="Launch!", command=lambda: self.done(True)) self.btcancel = ttk.Button(master, text="Cancel", command=self.done) self.btconfirm.grid(row=4, padx=(0, 3), sticky="news") self.btcancel.grid(row=4, column=1, padx=(3, 0), sticky="news") self._load_users_ui() self.userselectvar.trace("w", self.userchange) for i, t in enumerate(zip(self.users, self.users_str)): if self.remember_1_hackish == t[0][0]: self.userselectbox.current(i) del self.remember_1_hackish break else: if self.users: self.userselectbox.current(0) self._toggle_hlae_cb()
class LaunchTF2(BaseDialog): """ Dialog that reads and displays TF2 launch arguments and steam profile information, offers ability to change those and launch TF2 with an additional command that plays the demo on the game's startup, or directly hook HLAE into the game. After the dialog is closed: `self.result.state` will be SUCCESS if user hit launch, else FAILURE. `self.result.data` will be a dict where: "game_launched": Whether tf2 was launched (bool) "steampath": The text in the steampath entry, may have been changed by the user. (str) "hlaepath": The text in the hlaepath entry, may have been changed by the user. (str) Widget state remembering: 0: HLAE launch checkbox state (bool) 1: Selected userprofile (str) (ignored when not existing) """ REMEMBER_DEFAULT = [False, ""] REQUIRED_CFG_KEYS = ("steampath", "hlaepath", "rcon_pwd", "rcon_port") def __init__(self, parent, demopath, cfg, style, remember): """ parent: Parent widget, should be a `Tk` or `Toplevel` instance. demopath: Absolute file path to the demo to be played. (str) cfg: The program configuration. (dict) style: ttk.Style object remember: List of arbitrary values. See class docstring for details. """ super().__init__(parent, "Play demo / Launch TF2...") self.demopath = demopath self.steamdir_var = tk.StringVar() self.steamdir_var.set(cfg["steampath"]) self.hlaedir_var = tk.StringVar() self.hlaedir_var.set(cfg["hlaepath"]) self.usehlae_var = tk.BooleanVar() self.playdemoarg = tk.StringVar() self.userselectvar = tk.StringVar() self.launchoptionsvar = tk.StringVar() self._style = style self.cfg = cfg self.spinneriter = cycle((chain( *(repeat(sign, 100 // CNST.GUI_UPDATE_WAIT) for sign in ("|", "/", "-", "\\")), ))) self.rcon_threadgroup = ThreadGroup(RCONThread, self) self.rcon_threadgroup.register_run_always_method( self._rcon_after_run_always) self.rcon_threadgroup.decorate_and_patch(self, self._rcon_after_callback) self.errstates = [False for _ in range(4)] # 0: Bad config/steamdir, 1: Steamdir on bad drive, 2: Demo outside /tf/, # 3: No launchoptions, u_r = self.validate_and_update_remember(remember) self.usehlae_var.set(u_r[0]) self.remember_1_hackish = u_r[1] # Used at the end of body() self.shortdemopath = "" def body(self, master): """UI""" self.protocol("WM_DELETE_WINDOW", self.done) master.grid_columnconfigure((0, 1), weight=1) dir_sel_lblfrm = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label( master, "Paths")) dir_sel_lblfrm.grid_columnconfigure(0, weight=1) ds_widgetframe = ttk.Frame(dir_sel_lblfrm, style="Contained.TFrame") ds_widgetframe.grid_columnconfigure(1, weight=1) for i, j in enumerate(( ("Steam:", self.steamdir_var), ("HLAE:", self.hlaedir_var), )): dir_label = ttk.Label(ds_widgetframe, style="Contained.TLabel", text=j[0]) dir_entry = ttk.Entry(ds_widgetframe, state="readonly", textvariable=j[1]) def tmp_handler(self=self, var=j[1]): return self._sel_dir(var) dir_btn = ttk.Button(ds_widgetframe, style="Contained.TButton", command=tmp_handler, text="Change path...") dir_label.grid(row=i, column=0) dir_entry.grid(row=i, column=1, sticky="ew") dir_btn.grid(row=i, column=2, padx=(3, 0)) self.error_steamdir_invalid = ttk.Label( dir_sel_lblfrm, anchor = tk.N, justify = tk.CENTER, style = "Error.Contained.TLabel", text = "Getting steam users failed! Please select the root folder called " \ "\"Steam\".\nEventually check for permission conflicts." ) self.warning_steamdir_mislocated = ttk.Label( dir_sel_lblfrm, anchor=tk.N, style="Warning.Contained.TLabel", text= "The queried demo and the Steam directory are on seperate drives.") userselectframe = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label( master, "Select user profile if needed")) userselectframe.grid_columnconfigure(0, weight=1) self.info_launchoptions_not_found = ttk.Label( userselectframe, anchor=tk.N, style="Info.Contained.TLabel", text="Launch configuration not found, it likely does not exist.") self.userselectbox = ttk.Combobox(userselectframe, textvariable=self.userselectvar, state="readonly") # Once changed, observer callback triggered by self.userselectvar launchoptionsframe = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label( master, "Launch options")) launchoptionsframe.grid_columnconfigure(0, weight=1) launchoptwidgetframe = ttk.Frame(launchoptionsframe, borderwidth=4, relief=tk.RAISED, padding=(5, 4, 5, 4)) launchoptwidgetframe.grid_columnconfigure((1, ), weight=10) self.head_args_lbl = ttk.Label(launchoptwidgetframe, text="[...]/hl2.exe -steam -game tf") self.launchoptionsentry = ttk.Entry(launchoptwidgetframe, textvariable=self.launchoptionsvar) pluslabel = ttk.Label(launchoptwidgetframe, text="+") self.demo_play_arg_entry = ttk.Entry(launchoptwidgetframe, state="readonly", textvariable=self.playdemoarg) self.end_q_mark_label = ttk.Label(launchoptwidgetframe, text="") #launchoptwidgetframe.grid_propagate(False) self.use_hlae_checkbox = ttk.Checkbutton( launchoptionsframe, variable=self.usehlae_var, text="Launch using HLAE", style="Contained.TCheckbutton", command=self._toggle_hlae_cb) self.warning_not_in_tf_dir = ttk.Label( launchoptionsframe, anchor=tk.N, style="Warning.Contained.TLabel", text= "The demo can not be played as it is not in Team Fortress' file system (/tf)" ) # self.demo_play_arg_entry.config(width = len(self.playdemoarg.get()) + 2) rconlabelframe = ttk.LabelFrame(master, padding=(10, 8, 10, 8), labelwidget=frmd_label(master, "RCON")) rconlabelframe.grid_columnconfigure(1, weight=1) self.rcon_btn = ttk.Button(rconlabelframe, text="Send command", command=self._rcon, width=15, style="Centered.TButton") self.rcon_btn.grid(row=0, column=0) self.rcon_txt = TtkText(rconlabelframe, self._style, height=4, width=48, wrap=tk.CHAR) rcon_scrollbar = ttk.Scrollbar(rconlabelframe, orient=tk.HORIZONTAL, command=self.rcon_txt.xview) self.rcon_txt.insert(tk.END, "Status: [.]\n\n\n") self.rcon_txt.mark_set("spinner", "1.9") self.rcon_txt.mark_gravity("spinner", tk.LEFT) self.rcon_txt.configure(xscrollcommand=rcon_scrollbar.set, state=tk.DISABLED) self.rcon_txt.grid(row=0, column=1, sticky="news", padx=(5, 0)) rcon_scrollbar.grid(row=1, column=1, sticky="ew", padx=(5, 0)) # grid start # dir selection widgets are already gridded ds_widgetframe.grid(sticky="news") self.error_steamdir_invalid.grid() self.warning_steamdir_mislocated.grid() dir_sel_lblfrm.grid(columnspan=2, pady=5, sticky="news") self.userselectbox.grid(sticky="we") self.info_launchoptions_not_found.grid() userselectframe.grid(columnspan=2, pady=5, sticky="news") self.head_args_lbl.grid(row=0, column=0, columnspan=3, sticky="news") self.launchoptionsentry.grid(row=1, column=0, columnspan=3, sticky="news") pluslabel.grid(row=2, column=0) self.demo_play_arg_entry.grid(row=2, column=1, sticky="news") self.end_q_mark_label.grid(row=2, column=2) launchoptwidgetframe.grid(sticky="news") self.use_hlae_checkbox.grid(sticky="w", pady=(5, 0), ipadx=4) self.warning_not_in_tf_dir.grid() launchoptionsframe.grid(columnspan=2, pady=(5, 10), sticky="news") rconlabelframe.grid(columnspan=2, pady=(5, 10), sticky="news") self.btconfirm = ttk.Button(master, text="Launch!", command=lambda: self.done(True)) self.btcancel = ttk.Button(master, text="Cancel", command=self.done) self.btconfirm.grid(row=4, padx=(0, 3), sticky="news") self.btcancel.grid(row=4, column=1, padx=(3, 0), sticky="news") self._load_users_ui() self.userselectvar.trace("w", self.userchange) for i, t in enumerate(zip(self.users, self.users_str)): if self.remember_1_hackish == t[0][0]: self.userselectbox.current(i) del self.remember_1_hackish break else: if self.users: self.userselectbox.current(0) self._toggle_hlae_cb() def _showerrs(self): """ Go through all error conditions and update their respective error labels. """ for i, label in enumerate(( self.error_steamdir_invalid, self.warning_steamdir_mislocated, self.warning_not_in_tf_dir, self.info_launchoptions_not_found, )): if self.errstates[i]: label.grid() else: label.grid_forget() def getusers(self): """ Retrieve users from the current steam directory. If vdf module is present, returns a list of tuples where ([FOLDER_NAME], [USER_NAME]); if an error getting the user name occurs, the username will be None. Executed once by body(), by _sel_dir(), and used to insert value into self.userselectvar. """ toget = os.path.join(self.steamdir_var.get(), CNST.STEAM_CFG_PATH0) try: users = os.listdir(toget) except (OSError, PermissionError, FileNotFoundError): self.errstates[ERR_IDX.STEAMDIR] = True return [] for index, user in enumerate(users): try: cnf_file = os.path.join(toget, user, CNST.STEAM_CFG_PATH1) with open(cnf_file, encoding="utf-8") as h: vdfdata = vdf.load(h) username = eval(f"vdfdata{CNST.STEAM_CFG_USER_NAME}") username = tk_secure_str(username) users[index] = (users[index], username) except (OSError, PermissionError, FileNotFoundError, KeyError, SyntaxError): users[index] = (users[index], None) self.errstates[ERR_IDX.STEAMDIR] = False return users def _getlaunchoptions(self): try: tmp = self.userselectbox.current() if tmp == -1: raise KeyError("Bad or empty (?) steam folder") user_id = self.users[tmp][0] with open(os.path.join(self.steamdir_var.get(), CNST.STEAM_CFG_PATH0, user_id, CNST.STEAM_CFG_PATH1), encoding="utf-8") as h: subelem = vdf.load(h) for i in CNST.LAUNCHOPTIONSKEYS: if i in subelem: subelem = subelem[i] elif i.lower() in subelem: subelem = subelem[i.lower()] else: raise KeyError("Could not find launch options in vdf.") self.errstates[ERR_IDX.LAUNCHOPT] = False return subelem # SyntaxError raised by vdf module except (KeyError, FileNotFoundError, OSError, PermissionError, SyntaxError) as e: self.errstates[ERR_IDX.LAUNCHOPT] = True return "" def _sel_dir( self, variable): # Triggered by user clicking on the Dir choosing btn """ Opens up a file selection dialog. If the changed variable was the steam dir one, updates related widgets. """ sel = tk_fid.askdirectory() if sel == "": return variable.set(sel) if variable != self.steamdir_var: return self._load_users_ui() def _load_users_ui(self): """ Get users, store them in `self.users`, update related widgets. Also creates a list of strings as inserted into the combobox, stored in `self.users_str` """ self.users = self.getusers() self.users_str = [ t[0] + (f" - {t[1]}" if t[1] is not None else "") for t in self.users ] self.userselectbox.config(values=self.users_str) if self.users: self.userselectbox.current(0) else: self.userselectvar.set("") self._constructshortdemopath() self._showerrs() def _toggle_hlae_cb(self): """Changes some labels.""" if self.usehlae_var.get(): self.head_args_lbl.configure( text= "[...]/hlae.exe [...] -cmdLine \" -steam -game tf -insecure +sv_lan 1" ) self.end_q_mark_label.configure(text='"') else: self.head_args_lbl.configure(text="[...]/hl2.exe -steam -game tf") self.end_q_mark_label.configure(text="") def userchange(self, *_): # Triggered by observer on combobox variable. """Callback to retrieve launch options and update error labels.""" launchopt = self._getlaunchoptions() self.launchoptionsvar.set(launchopt) self._showerrs() def _constructshortdemopath(self): try: self.shortdemopath = os.path.relpath( self.demopath, os.path.join(self.steamdir_var.get(), CNST.TF2_HEAD_PATH)) self.errstates[ERR_IDX.STEAMDIR_DRIVE] = False except ValueError: self.shortdemopath = "" self.errstates[ERR_IDX.STEAMDIR_DRIVE] = True self.errstates[ERR_IDX.DEMO_OUTSIDE_GAME] = True if ".." in self.shortdemopath: self.errstates[ERR_IDX.DEMO_OUTSIDE_GAME] = True else: self.errstates[ERR_IDX.DEMO_OUTSIDE_GAME] = False self.playdemoarg.set("playdemo " + self.shortdemopath) def _rcon_txt_set_line(self, n, content): """ Set line n (0-2) of rcon txt widget to content. """ with self.rcon_txt: self.rcon_txt.delete(f"{n + 2}.0", f"{n + 2}.{tk.END}") self.rcon_txt.insert(f"{n + 2}.0", content) def _rcon(self): self.rcon_btn.configure(text="Cancel", command=self._cancel_rcon) self.rcon_txt.configure(state=tk.NORMAL) for i in range(3): self._rcon_txt_set_line(i, "") self.rcon_txt.configure(state=tk.DISABLED) self.rcon_threadgroup.start_thread( command=f"playdemo {self.shortdemopath}", password=self.cfg["rcon_pwd"], port=self.cfg["rcon_port"], ) def _cancel_rcon(self): self.rcon_threadgroup.join_thread() def _rcon_after_callback(self, queue_elem): if queue_elem[0] < 0x100: # Finish self.rcon_btn.configure(text="Send command", command=self._rcon) if queue_elem[0] == THREADSIG.ABORTED: for i in range(3): self._rcon_txt_set_line(i, "") with self.rcon_txt: self.rcon_txt.delete("spinner", "spinner + 1 chars") self.rcon_txt.insert("spinner", ".") return THREADGROUPSIG.FINISHED elif queue_elem[0] == THREADSIG.INFO_IDX_PARAM: self._rcon_txt_set_line(queue_elem[1], queue_elem[2]) return THREADGROUPSIG.CONTINUE def _rcon_after_run_always(self): with self.rcon_txt: self.rcon_txt.delete("spinner", "spinner + 1 chars") self.rcon_txt.insert("spinner", next(self.spinneriter)) def done(self, launch=False): self._cancel_rcon() self.result.state = DIAGSIG.SUCCESS if launch else DIAGSIG.FAILURE if launch: USE_HLAE = self.usehlae_var.get() user_args = self.launchoptionsvar.get().split() # args for hl2.exe tf2_launch_args = CNST.TF2_LAUNCHARGS + user_args + [ "+playdemo", self.shortdemopath ] if USE_HLAE: tf2_launch_args.extend(CNST.HLAE_ADD_TF2_ARGS) executable = os.path.join(self.hlaedir_var.get(), CNST.HLAE_EXE) launch_args = CNST.HLAE_LAUNCHARGS0.copy() # hookdll required launch_args.append( os.path.join(self.hlaedir_var.get(), CNST.HLAE_HOOK_DLL)) # hl2 exe path required launch_args.extend(CNST.HLAE_LAUNCHARGS1) launch_args.append( os.path.join(self.steamdir_var.get(), CNST.TF2_EXE_PATH)) launch_args.extend(CNST.HLAE_LAUNCHARGS2) # has to be supplied as string launch_args.append(" ".join(tf2_launch_args)) else: executable = os.path.join(self.steamdir_var.get(), CNST.TF2_EXE_PATH) launch_args = tf2_launch_args final_launchoptions = [executable] + launch_args self.result.data = { "steampath": self.steamdir_var.get(), "hlaepath": self.hlaedir_var.get(), } try: subprocess.Popen(final_launchoptions) #-steam param may cause conflicts when steam is not open but what do I know? self.result.data["game_launched"] = True except FileNotFoundError: self.result.data["game_launched"] = False tk_msg.showerror("Demomgr - Error", "Executable not found.", parent=self) except (OSError, PermissionError) as error: self.result.data["game_launched"] = False tk_msg.showerror("Demomgr - Error", f"Could not access executable :\n{error}", parent=self) self.result.remember = [ self.usehlae_var.get(), self.users[self.userselectbox.current()][0] \ if self.userselectbox.current() != -1 else "" ] self.destroy()
def body(self, parent): """UI setup, listbox filling.""" self.protocol("WM_DELETE_WINDOW", self.destroy) parent.rowconfigure(0, weight=1, pad=5) parent.rowconfigure(1, pad=5) parent.columnconfigure((0, 1), weight=1) widgetcontainer = ttk.Frame(parent) #, style = "Contained.TFrame") self.bind("<<MultiframeSelect>>", self._callback_bookmark_selected) widgetcontainer.columnconfigure(0, weight=4) widgetcontainer.columnconfigure(1, weight=1) widgetcontainer.columnconfigure(2, weight=1) widgetcontainer.rowconfigure(3, weight=1) self.listbox = mfl.MultiframeList(widgetcontainer, inicolumns=( { "name": "Name", "col_id": "col_name" }, { "name": "Tick", "col_id": "col_tick" }, )) self.listbox.grid(row=0, column=0, rowspan=4, sticky="news") insert_opt_lblfrm = ttk.Labelframe(widgetcontainer, labelwidget=frmd_label( widgetcontainer, "Bookmark data:")) insert_opt_lblfrm.columnconfigure(1, weight=1) ttk.Label(insert_opt_lblfrm, style="Contained.TLabel", text="Name:").grid(row=0, column=0) ttk.Label(insert_opt_lblfrm, style="Contained.TLabel", text="Tick:").grid(row=1, column=0) self.name_entry = ttk.Entry( insert_opt_lblfrm, validate="key", validatecommand=(parent.register(name_validator), "%P")) self.tick_entry = ttk.Entry( insert_opt_lblfrm, validate="key", validatecommand=(parent.register(int_validator), "%S", "%P")) apply_btn = ttk.Button(insert_opt_lblfrm, text="Apply", command=self._apply_changes) self.name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5) self.tick_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5) self.name_entry.bind("<Return>", self._apply_changes) self.tick_entry.bind("<Return>", self._apply_changes) apply_btn.grid(row=2, column=1, sticky="ew", padx=5, pady=5) insert_opt_lblfrm.grid(row=0, column=1, sticky="ew", padx=(5, 0), columnspan=2) add_bm_btn = ttk.Button(widgetcontainer, text="New", command=self._add_bookmark) rem_bm_btn = ttk.Button(widgetcontainer, text="Remove", command=self._rem_bookmark) add_bm_btn.grid(row=1, column=1, sticky="ew", padx=(5, 0), pady=5) rem_bm_btn.grid(row=1, column=2, sticky="ew", padx=(5, 0), pady=5) save_loc_lblfrm = ttk.Labelframe(widgetcontainer, labelwidget=frmd_label( widgetcontainer, "Save changes to:")) json_checkbox = ttk.Checkbutton(save_loc_lblfrm, text=".json", variable=self.jsonmark_var, style="Contained.TCheckbutton") events_checkbox = ttk.Checkbutton(save_loc_lblfrm, text=CNST.EVENT_FILE, variable=self.eventsmark_var, style="Contained.TCheckbutton") json_checkbox.grid(sticky="w", ipadx=2, padx=5, pady=5) events_checkbox.grid(sticky="w", ipadx=2, padx=5, pady=5) save_loc_lblfrm.grid(row=2, column=1, sticky="ew", padx=(5, 0), columnspan=2) widgetcontainer.grid(row=0, column=0, columnspan=2, sticky="news") self.textbox = TtkText(parent, self.styleobj, height=8, wrap="none", takefocus=False) self.textbox.grid(row=1, column=0, columnspan=2, sticky="news", pady=5) self.textbox.lower() self.savebtn = ttk.Button(parent, text="Save", command=self._mark) cancelbtn = ttk.Button(parent, text="Close", command=self.destroy) self.savebtn.grid(row=2, column=0, padx=(0, 3), sticky="ew") cancelbtn.grid(row=2, column=1, padx=(3, 0), sticky="ew") self._fill_gui() self._log(f"Marking {os.path.split(self.targetdemo)[1]}\n")
class BookmarkSetter(BaseDialog): """ Dialog that offers ability to modify and then write demo information into a demo's json file or _events.txt entry. After the dialog is closed: If the thread succeeded at least once, `self.result.state` is SUCCESS, the new bookmark tuple can be found in `self.result.data["bookmarks"]` in the usual primitive format. These are not guaranteed to be the bookmarks on disk, but the ones entered in the UI by the user. `self.result.data["containers"]` will be a 2-value tuple of booleans denoting the state of information containers, True if a container now exists, False if it doesn't and None if something went wrong, in which case the container's existence is unchanged. Containers are 0: _events.txt; 1: json file If the thread failed or wasn't even started, `self.result.data` will be an empty dict. Widget state remembering: 0: json checkbox (bool) 1: _events.txt checkbox (bool) """ REMEMBER_DEFAULT = [False, False] def __init__(self, parent, targetdemo, bm_dat, styleobj, evtblocksz, remember): """ parent: Parent widget, should be a `Tk` or `Toplevel` instance. targetdemo: Full path to the demo that should be marked. bm_dat: Bookmarks for the specified demo in the usual info format (((killstreak_peak, tick), ...), ((bookmark_name, tick), ...)) styleobj: Instance of `tkinter.ttk.Style` evtblocksz: Size of blocks to read _events.txt in. remember: List of arbitrary values. See class docstring for details. """ super().__init__(parent, "Insert bookmark...") self.result.data = {} self.targetdemo = targetdemo self.demo_dir = os.path.dirname(targetdemo) self.bm_dat = bm_dat self.styleobj = styleobj self.evtblocksz = evtblocksz u_r = self.validate_and_update_remember(remember) self.jsonmark_var = tk.BooleanVar() self.eventsmark_var = tk.BooleanVar() self.jsonmark_var.set(u_r[0]) self.eventsmark_var.set(u_r[1]) self.threadgroup = ThreadGroup(ThreadMarkDemo, parent) self.threadgroup.decorate_and_patch(self, self._mark_after_callback) def body(self, parent): """UI setup, listbox filling.""" self.protocol("WM_DELETE_WINDOW", self.destroy) parent.rowconfigure(0, weight=1, pad=5) parent.rowconfigure(1, pad=5) parent.columnconfigure((0, 1), weight=1) widgetcontainer = ttk.Frame(parent) #, style = "Contained.TFrame") self.bind("<<MultiframeSelect>>", self._callback_bookmark_selected) widgetcontainer.columnconfigure(0, weight=4) widgetcontainer.columnconfigure(1, weight=1) widgetcontainer.columnconfigure(2, weight=1) widgetcontainer.rowconfigure(3, weight=1) self.listbox = mfl.MultiframeList(widgetcontainer, inicolumns=( { "name": "Name", "col_id": "col_name" }, { "name": "Tick", "col_id": "col_tick" }, )) self.listbox.grid(row=0, column=0, rowspan=4, sticky="news") insert_opt_lblfrm = ttk.Labelframe(widgetcontainer, labelwidget=frmd_label( widgetcontainer, "Bookmark data:")) insert_opt_lblfrm.columnconfigure(1, weight=1) ttk.Label(insert_opt_lblfrm, style="Contained.TLabel", text="Name:").grid(row=0, column=0) ttk.Label(insert_opt_lblfrm, style="Contained.TLabel", text="Tick:").grid(row=1, column=0) self.name_entry = ttk.Entry( insert_opt_lblfrm, validate="key", validatecommand=(parent.register(name_validator), "%P")) self.tick_entry = ttk.Entry( insert_opt_lblfrm, validate="key", validatecommand=(parent.register(int_validator), "%S", "%P")) apply_btn = ttk.Button(insert_opt_lblfrm, text="Apply", command=self._apply_changes) self.name_entry.grid(row=0, column=1, sticky="ew", padx=5, pady=5) self.tick_entry.grid(row=1, column=1, sticky="ew", padx=5, pady=5) self.name_entry.bind("<Return>", self._apply_changes) self.tick_entry.bind("<Return>", self._apply_changes) apply_btn.grid(row=2, column=1, sticky="ew", padx=5, pady=5) insert_opt_lblfrm.grid(row=0, column=1, sticky="ew", padx=(5, 0), columnspan=2) add_bm_btn = ttk.Button(widgetcontainer, text="New", command=self._add_bookmark) rem_bm_btn = ttk.Button(widgetcontainer, text="Remove", command=self._rem_bookmark) add_bm_btn.grid(row=1, column=1, sticky="ew", padx=(5, 0), pady=5) rem_bm_btn.grid(row=1, column=2, sticky="ew", padx=(5, 0), pady=5) save_loc_lblfrm = ttk.Labelframe(widgetcontainer, labelwidget=frmd_label( widgetcontainer, "Save changes to:")) json_checkbox = ttk.Checkbutton(save_loc_lblfrm, text=".json", variable=self.jsonmark_var, style="Contained.TCheckbutton") events_checkbox = ttk.Checkbutton(save_loc_lblfrm, text=CNST.EVENT_FILE, variable=self.eventsmark_var, style="Contained.TCheckbutton") json_checkbox.grid(sticky="w", ipadx=2, padx=5, pady=5) events_checkbox.grid(sticky="w", ipadx=2, padx=5, pady=5) save_loc_lblfrm.grid(row=2, column=1, sticky="ew", padx=(5, 0), columnspan=2) widgetcontainer.grid(row=0, column=0, columnspan=2, sticky="news") self.textbox = TtkText(parent, self.styleobj, height=8, wrap="none", takefocus=False) self.textbox.grid(row=1, column=0, columnspan=2, sticky="news", pady=5) self.textbox.lower() self.savebtn = ttk.Button(parent, text="Save", command=self._mark) cancelbtn = ttk.Button(parent, text="Close", command=self.destroy) self.savebtn.grid(row=2, column=0, padx=(0, 3), sticky="ew") cancelbtn.grid(row=2, column=1, padx=(3, 0), sticky="ew") self._fill_gui() self._log(f"Marking {os.path.split(self.targetdemo)[1]}\n") def _add_bookmark(self): name = self.name_entry.get() tick = self.tick_entry.get() tick = int(tick) if tick else 0 self.listbox.insert_row( { "col_name": name, "col_tick": tick }, self._find_insertion_index(tick), ) def _apply_changes(self, *_): """ Apply user-entered name and tick to the mfl entry and reinsert it into the list at correct position. """ index = self.listbox.get_selected_cell()[1] if index is None: self._log("No bookmark selected to change.") return new_name, new_tick = self.name_entry.get(), self.tick_entry.get() new_tick = int(new_tick) if new_tick else 0 self.listbox.remove_row(index) new_idx = self._find_insertion_index(int(new_tick)) self.listbox.insert_row({ "col_name": new_name, "col_tick": new_tick }, new_idx) self.listbox.set_selected_cell(0, new_idx) def _callback_bookmark_selected(self, *_): self.name_entry.delete(0, tk.END) self.tick_entry.delete(0, tk.END) idx = self.listbox.get_selected_cell()[1] if idx is None: return data, col_idx = self.listbox.get_rows(idx) new_name, new_tick = data[0][col_idx["col_name"]], data[0][ col_idx["col_tick"]] self.name_entry.insert(0, new_name) self.tick_entry.insert(0, str(new_tick)) def _cancel_mark(self): self.threadgroup.join_thread() def _find_insertion_index(self, tick): """ Returns index to insert a new tick number at, assuming the list is sorted by tick ascending """ insidx = 0 for i in range(self.listbox.get_length()): if self.listbox.get_cell("col_tick", i) > tick: break insidx += 1 return insidx def _fill_gui(self): """Called by body, loads bookmarks into the listbox.""" if self.bm_dat is None: return for n, t in self.bm_dat: self.listbox.insert_row({"col_name": n, "col_tick": t}) def _log(self, tolog): """Inserts "\n" + tolog into self.textbox.""" with self.textbox: self.textbox.insert(tk.END, "\n" + tolog) if self.textbox.yview()[1] < 1.0: self.textbox.delete("1.0", "2.0") self.textbox.yview_moveto(1.0) def _mark(self): mark_json = self.jsonmark_var.get() mark_evts = self.eventsmark_var.get() raw_bookmarks = tuple( zip( self.listbox.get_column("col_name"), self.listbox.get_column("col_tick"), )) self.savebtn.configure(text="Cancel", command=self._cancel_mark) self.threadgroup.start_thread( mark_json=mark_json, mark_events=mark_evts, bookmarks=raw_bookmarks, targetdemo=self.targetdemo, evtblocksz=self.evtblocksz, ) def _mark_after_callback(self, queue_elem): if queue_elem[0] < 0x100: # Finish self.savebtn.configure(text="Save", command=self._mark) if queue_elem[0] == THREADSIG.SUCCESS: self.result.state = DIAGSIG.SUCCESS self.result.data["bookmarks"] = tuple( zip(self.listbox.get_column("col_name"), map(int, self.listbox.get_column("col_tick")))) return THREADGROUPSIG.FINISHED elif queue_elem[0] == THREADSIG.INFO_INFORMATION_CONTAINERS: self.result.data["containers"] = queue_elem[1] elif queue_elem[0] == THREADSIG.INFO_CONSOLE: self._log(queue_elem[1]) return THREADGROUPSIG.CONTINUE def _rem_bookmark(self): index = self.listbox.get_selected_cell()[1] if index is None: self._log("No bookmark to remove selected.") return self.listbox.remove_row(index) def destroy(self): self._cancel_mark() self.result.remember = [ self.jsonmark_var.get(), self.eventsmark_var.get() ] super().destroy()