Exemple #1
0
    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")
Exemple #2
0
    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))
Exemple #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()
Exemple #4
0
    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()
Exemple #5
0
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()
Exemple #6
0
    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")
Exemple #7
0
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()