def on_leave(slot: dragdrop.Slot[Signage]) -> None: """Reset the visible sign when left.""" nonlocal hover_toggle_id, hover_sign name_label['text'] = '' hover_sign = None if hover_toggle_id is not None: TK_ROOT.after_cancel(hover_toggle_id) hover_toggle_id = None img.apply(preview_left, IMG_BLANK) img.apply(preview_right, IMG_BLANK)
def __init__( self, man: Manager, parent: tkinter.Misc, is_source: bool, label: str, ) -> None: """Internal only, use Manager.slot().""" self.man = man self.is_source = is_source self._contents = None self._pos_type = None self._lbl = tkinter.Label(parent) img.apply(self._lbl, man._img_blank) tk_tools.bind_leftclick(self._lbl, self._evt_start) self._lbl.bind(tk_tools.EVENTS['LEFT_SHIFT'], self._evt_fastdrag) self._lbl.bind('<Enter>', self._evt_hover_enter) self._lbl.bind('<Leave>', self._evt_hover_exit) config_event = self._evt_configure tk_tools.bind_rightclick(self._lbl, config_event) if label: self._text_lbl = tkinter.Label( self._lbl, text=label, font=('Helvetica', -12), relief='ridge', bg=img.PETI_ITEM_BG_HEX, ) else: self._text_lbl = None if man.config_icon: self._info_btn = tkinter.Label( self._lbl, relief='ridge', ) img.apply(self._info_btn, img.Handle.builtin('icons/gear', 10, 10)) @tk_tools.bind_leftclick(self._info_btn) def info_button_click(e): """Trigger the callback whenever the gear button was pressed.""" config_event(e) # Cancel the event sequence, so it doesn't travel up to the main # window and hide the window again. return 'break' # Rightclick does the same as the main icon. tk_tools.bind_rightclick(self._info_btn, config_event) else: self._info_btn = None
def _display_item( self, lbl: Union[tkinter.Label, ttk.Label], item: Optional[ItemT], group: bool=False, ) -> None: """Display the specified item on the given label.""" if item is None: image = self._img_blank elif group: try: image = item.dnd_group_icon except AttributeError: image = item.dnd_icon else: image = item.dnd_icon img.apply(lbl, image)
def __init__( self, master: tkinter.Toplevel, *, size: Tuple[int, int]=(64, 64), config_icon: bool=False ): """Create a group of drag-drop slots. size is the size of each moved image. If config_icon is set, gear icons will be added to each slot to configure items. This indicates the right-click option is available, and makes it easier to press that. """ self.width, self.height = size self._targets = [] # type: List[Slot[ItemT]] self._sources = [] # type: List[Slot[ItemT]] self._img_blank = img.Handle.color(img.PETI_ITEM_BG, *size) self.config_icon = config_icon # If dragging, the item we are dragging. self._cur_drag = None # type: Optional[ItemT] # While dragging, the place we started at. self._cur_prev_slot = None # type: Optional[Slot[ItemT]] self._callbacks = { event: [] for event in Event } # type: Dict[Event, List[Callable[[Slot], None]]] self._drag_win = drag_win = tkinter.Toplevel(master) drag_win.withdraw() drag_win.transient(master=master) drag_win.wm_overrideredirect(True) self._drag_lbl = drag_lbl = tkinter.Label(drag_win) img.apply(drag_lbl, self._img_blank) drag_lbl.grid(row=0, column=0) drag_win.bind(tk_tools.EVENTS['LEFT_RELEASE'], self._evt_stop)
def hover_toggle() -> None: """Toggle between arrows and dual icons.""" nonlocal hover_arrow, hover_toggle_id hover_arrow = not hover_arrow if hover_sign is None: return if hover_arrow and sign_arrow: left = hover_sign.dnd_icon right = sign_arrow.dnd_icon else: try: left = Signage.by_id(hover_sign.prim_id or '').dnd_icon except KeyError: left = hover_sign.dnd_icon try: right = Signage.by_id(hover_sign.sec_id or '').dnd_icon except KeyError: right = IMG_BLANK img.apply(preview_left, left) img.apply(preview_right, right) hover_toggle_id = TK_ROOT.after(1000, hover_toggle)
def unload_icons(self) -> None: """Reset all icons to blank. This way they can be destroyed.""" for slot in self._sources: img.apply(slot._lbl, self._img_blank) for slot in self._targets: img.apply(slot._lbl, self._img_blank) img.apply(self._drag_lbl, self._img_blank)
def update_image(var_name: str, var_index: str, operation: str): img.apply(swatch, img.Handle.color(parse_color(var.get()), size, size))
def hide_window() -> None: """Hide the window.""" window.withdraw() drag_man.unload_icons() img.apply(preview_left, IMG_BLANK) img.apply(preview_right, IMG_BLANK)
def init_widgets(master: ttk.Frame) -> Optional[tk.Widget]: """Construct the widgets, returning the configuration button. If no signages are defined, this returns None. """ if not any(Signage.all()): return ttk.Label(master) window.resizable(True, True) window.title(gettext('Configure Signage')) frame_selected = ttk.Labelframe( window, text=gettext('Selected'), relief='raised', labelanchor='n', ) canv_all = tk.Canvas(window) scroll = tk_tools.HidingScroll(window, orient='vertical', command=canv_all.yview) canv_all['yscrollcommand'] = scroll.set name_label = ttk.Label(window, text='', justify='center') frame_preview = ttk.Frame(window, relief='raised', borderwidth=4) frame_selected.grid(row=0, column=0, sticky='nsew') ttk.Separator(orient='horizontal').grid(row=1, column=0, sticky='ew') name_label.grid(row=2, column=0) frame_preview.grid(row=3, column=0, pady=4) canv_all.grid(row=0, column=1, rowspan=4, sticky='nsew') scroll.grid(row=0, column=2, rowspan=4, sticky='ns') window.columnconfigure(1, weight=1) window.rowconfigure(3, weight=1) tk_tools.add_mousewheel(canv_all, canv_all, window) preview_left = ttk.Label(frame_preview, anchor='e') preview_right = ttk.Label(frame_preview, anchor='w') img.apply(preview_left, IMG_BLANK) img.apply(preview_right, IMG_BLANK) preview_left.grid(row=0, column=0) preview_right.grid(row=0, column=1) try: sign_arrow = Signage.by_id('SIGN_ARROW') except KeyError: LOGGER.warning('No arrow signage defined!') sign_arrow = None hover_arrow = False hover_toggle_id = None hover_sign: Optional[Signage] = None def hover_toggle() -> None: """Toggle between arrows and dual icons.""" nonlocal hover_arrow, hover_toggle_id hover_arrow = not hover_arrow if hover_sign is None: return if hover_arrow and sign_arrow: left = hover_sign.dnd_icon right = sign_arrow.dnd_icon else: try: left = Signage.by_id(hover_sign.prim_id or '').dnd_icon except KeyError: left = hover_sign.dnd_icon try: right = Signage.by_id(hover_sign.sec_id or '').dnd_icon except KeyError: right = IMG_BLANK img.apply(preview_left, left) img.apply(preview_right, right) hover_toggle_id = TK_ROOT.after(1000, hover_toggle) def on_hover(slot: dragdrop.Slot[Signage]) -> None: """Show the signage when hovered.""" nonlocal hover_arrow, hover_sign if slot.contents is not None: name_label['text'] = gettext('Signage: {}').format(slot.contents.name) hover_sign = slot.contents hover_arrow = True hover_toggle() else: on_leave(slot) def on_leave(slot: dragdrop.Slot[Signage]) -> None: """Reset the visible sign when left.""" nonlocal hover_toggle_id, hover_sign name_label['text'] = '' hover_sign = None if hover_toggle_id is not None: TK_ROOT.after_cancel(hover_toggle_id) hover_toggle_id = None img.apply(preview_left, IMG_BLANK) img.apply(preview_right, IMG_BLANK) drag_man.reg_callback(dragdrop.Event.HOVER_ENTER, on_hover) drag_man.reg_callback(dragdrop.Event.HOVER_EXIT, on_leave) for i in range(3, 31): SLOTS_SELECTED[i] = slot = drag_man.slot( frame_selected, source=False, label=f'00:{i:02g}' ) row, col = divmod(i-3, 4) slot.grid(row=row, column=col, padx=1, pady=1) prev_id = DEFAULT_IDS.get(i, '') if prev_id: try: slot.contents = Signage.by_id(prev_id) except KeyError: LOGGER.warning('Missing sign id: {}', prev_id) for sign in sorted(Signage.all(), key=lambda s: s.name): if not sign.hidden: slot = drag_man.slot(canv_all, source=True) slot.contents = sign drag_man.flow_slots(canv_all, drag_man.sources()) canv_all.bind( '<Configure>', lambda e: drag_man.flow_slots(canv_all, drag_man.sources()), ) def hide_window() -> None: """Hide the window.""" window.withdraw() drag_man.unload_icons() img.apply(preview_left, IMG_BLANK) img.apply(preview_right, IMG_BLANK) def show_window() -> None: """Show the window.""" drag_man.load_icons() window.deiconify() utils.center_win(window, TK_ROOT) window.protocol("WM_DELETE_WINDOW", hide_window) return ttk.Button( master, text=gettext('Configure Signage'), command=show_window, )
def _show(widget: tk.Misc, mouse_x, mouse_y) -> None: """Show the context window.""" # noinspection PyUnresolvedReferences, PyProtectedMember context_label['text'] = widget._bee2_tooltip_text # noinspection PyUnresolvedReferences, PyProtectedMember img.apply(context_label, widget._bee2_tooltip_img) window.deiconify() window.update_idletasks() window.lift() # We're going to position tooltips towards the center of the main window. # That way they don't tend to stick out, even in multi-window setups. # To decide where to put the tooltip, we first want the center of the # main window. x = cent_x = TK_ROOT.winfo_rootx() + TK_ROOT.winfo_width() / 2 y = cent_y = TK_ROOT.winfo_rooty() + TK_ROOT.winfo_height() / 2 x_centered = y_centered = True # If the widget is smaller than the context window, always center. if widget.winfo_width() > window.winfo_width(): if cent_x > mouse_x + CENT_DIST: # Left of center, so place right of the target x = widget.winfo_rootx() + widget.winfo_width() + PADDING x_centered = False elif cent_x < mouse_x - CENT_DIST: # Right of center, so place left of the target x = widget.winfo_rootx() - window.winfo_width() - PADDING x_centered = False if widget.winfo_height() > window.winfo_height(): if cent_y > mouse_y + CENT_DIST: # Above center, so place below target y = widget.winfo_rooty() + widget.winfo_height() + PADDING y_centered = False elif cent_y < mouse_y - CENT_DIST: # Below center, so place above target y = widget.winfo_rooty() - window.winfo_height() - PADDING y_centered = False if x_centered: # Center horizontally x = ( widget.winfo_rootx() + (widget.winfo_width() - window.winfo_width()) // 2 ) if y_centered: y = ( widget.winfo_rooty() + (widget.winfo_height() - window.winfo_height()) // 2 ) # If both X and Y are centered, the tooltip will appear on top of # the mouse and immediately hide. Offset it to fix that. if x_centered: if mouse_y < cent_y: y = widget.winfo_rooty() + widget.winfo_height() + PADDING else: y = widget.winfo_rooty() - window.winfo_height() - PADDING window.geometry('+{}+{}'.format(int(x), int(y)))
def init() -> None: """Initialise all widgets in the given window.""" for cat, btn_text in [ ('back_', gettext('Restore:')), ('game_', gettext('Backup:')), ]: UI[cat + 'frame'] = frame = ttk.Frame( window, ) UI[cat + 'title_frame'] = title_frame = ttk.Frame( frame, ) title_frame.grid(row=0, column=0, sticky='EW') UI[cat + 'title'] = ttk.Label( title_frame, font='TkHeadingFont', ) UI[cat + 'title'].grid(row=0, column=0) title_frame.rowconfigure(0, weight=1) title_frame.columnconfigure(0, weight=1) UI[cat + 'details'] = CheckDetails( frame, headers=HEADERS, ) UI[cat + 'details'].grid(row=1, column=0, sticky='NSEW') frame.rowconfigure(1, weight=1) frame.columnconfigure(0, weight=1) button_frame = ttk.Frame( frame, ) button_frame.grid(column=0, row=2) ttk.Label(button_frame, text=btn_text).grid(row=0, column=0) UI[cat + 'btn_all'] = ttk.Button( button_frame, text='All', width=3, ) UI[cat + 'btn_sel'] = ttk.Button( button_frame, text=gettext('Checked'), width=8, ) UI[cat + 'btn_all'].grid(row=0, column=1) UI[cat + 'btn_sel'].grid(row=0, column=2) UI[cat + 'btn_del'] = ttk.Button( button_frame, text=gettext('Delete Checked'), width=14, ) UI[cat + 'btn_del'].grid(row=1, column=0, columnspan=3) tk_tools.add_mousewheel( UI[cat + 'details'].wid_canvas, UI[cat + 'frame'], ) game_refresh = ttk.Button( UI['game_title_frame'], command=ui_refresh_game, ) game_refresh.grid(row=0, column=1, sticky='E') add_tooltip( game_refresh, "Reload the map list.", ) img.apply(game_refresh, img.Handle.builtin('icons/tool_sub', 16, 16)) UI['game_title']['textvariable'] = game_name UI['back_title']['textvariable'] = backup_name UI['game_btn_all']['command'] = ui_backup_all UI['game_btn_sel']['command'] = ui_backup_sel UI['game_btn_del']['command'] = ui_delete_game UI['back_btn_all']['command'] = ui_restore_all UI['back_btn_sel']['command'] = ui_restore_sel UI['back_btn_del']['command'] = ui_delete_backup UI['back_frame'].grid(row=1, column=0, sticky='NSEW') ttk.Separator(orient=tk.VERTICAL).grid( row=1, column=1, sticky='NS', padx=5, ) UI['game_frame'].grid(row=1, column=2, sticky='NSEW') window.rowconfigure(1, weight=1) window.columnconfigure(0, weight=1) window.columnconfigure(2, weight=1)
def make_tab(group, config: ConfigFile, tab_type): """Create all the widgets for a tab.""" if tab_type is TabTypes.MIDCHAMBER: # Mid-chamber voice lines have predefined values. group_name = gettext('Mid - Chamber') group_id = 'MIDCHAMBER' group_desc = gettext('Lines played during the actual chamber, ' 'after specific events have occurred.') elif tab_type is TabTypes.RESPONSE: # Note: 'Response' tab header, and description group_name = gettext('Responses') group_id = None group_desc = gettext( 'Lines played in response to certain events in Coop.') elif tab_type is TabTypes.NORM: group_name = group['name', 'No Name!'] group_id = group_name.upper() group_desc = group['desc', ''] + ':' else: raise ValueError('Invalid tab type!') # This is just to hold the canvas and scrollbar outer_frame = ttk.Frame(UI['tabs']) outer_frame.columnconfigure(0, weight=1) outer_frame.rowconfigure(0, weight=1) TABS[group_name] = outer_frame # We add this attribute so the refresh() method knows all the # tab names outer_frame.nb_text = group_name outer_frame.nb_type = tab_type # We need a canvas to make the list scrollable. canv = Canvas( outer_frame, highlightthickness=0, ) scroll = tk_tools.HidingScroll( outer_frame, orient=VERTICAL, command=canv.yview, ) canv['yscrollcommand'] = scroll.set canv.grid(row=0, column=0, sticky='NSEW') scroll.grid(row=0, column=1, sticky='NS') UI['tabs'].add(outer_frame) # This holds the actual elements frame = ttk.Frame(canv, ) frame.columnconfigure(0, weight=1) canv.create_window(0, 0, window=frame, anchor="nw") ttk.Label( frame, text=group_name, anchor='center', font='tkHeadingFont', ).grid( row=0, column=0, sticky='EW', ) ttk.Label( frame, text=group_desc, ).grid( row=1, column=0, sticky='EW', ) ttk.Separator(frame, orient=HORIZONTAL).grid( row=2, column=0, sticky='EW', ) if tab_type is TabTypes.RESPONSE: sorted_quotes = sorted(group, key=lambda prop: prop.real_name) else: sorted_quotes = sorted( group.find_all('Quote'), key=quote_sort_func, reverse=True, ) for quote in sorted_quotes: # type: Property if not quote.has_children(): continue # Skip over config commands.. if tab_type is TabTypes.RESPONSE: try: name = RESPONSE_NAMES[quote.name] except KeyError: # Convert channels of the form 'death_goo' into 'Death - Goo'. channel, ch_arg = quote.name.split('_', 1) name = channel.title() + ' - ' + ch_arg.title() del channel, ch_arg group_id = quote.name else: # note: default for quote names name = quote['name', gettext('No Name!')] ttk.Label( frame, text=name, font=QUOTE_FONT, ).grid( column=0, sticky=W, ) if tab_type is TabTypes.RESPONSE: line_iter = find_resp_lines(quote) else: line_iter = find_lines(quote) for badges, line, line_id in line_iter: line_frame = ttk.Frame(frame, ) line_frame.grid( column=0, padx=(10, 0), sticky=W, ) for x, (img_handle, ctx) in enumerate(badges): label = ttk.Label(line_frame, padding=0) img.apply(label, img_handle) label.grid(row=0, column=x) add_tooltip(label, ctx) line_frame.columnconfigure(len(badges), weight=1) check = ttk.Checkbutton( line_frame, # note: default voice line name next to checkbox. text=line['name', gettext('No Name?')], ) check.quote_var = IntVar(value=config.get_bool( group_id, line_id, True), ) check['variable'] = check.quote_var check['command'] = functools.partial( check_toggled, var=check.quote_var, config_section=config[group_id], quote_id=line_id, ) check.transcript = list(get_trans_lines(line)) check.grid( row=0, column=len(badges), ) check.bind("<Enter>", show_trans) def configure_canv(e): """Allow resizing the windows.""" canv['scrollregion'] = ( 4, 0, canv.winfo_reqwidth(), frame.winfo_reqheight(), ) frame['width'] = canv.winfo_reqwidth() canv.bind('<Configure>', configure_canv) return outer_frame