def __init__( self, master, is_dir: bool = False, loc: str = '', callback: Callable[[str], None] = None, ) -> None: """Initialise the field. - Set is_dir to true to look for directories, instead of files. - width sets the number of characters to display. - callback is a function to be called with the new path whenever it changes. """ from tooltip import add_tooltip super(FileField, self).__init__(master) self._location = loc self.is_dir = is_dir self._text_var = tk.StringVar(master=self, value='') if is_dir: self.browser = filedialog.Directory( self, initialdir=loc, ) # type: commondialog.Dialog else: self.browser = filedialog.SaveAs( self, initialdir=loc, ) if callback is None: callback = self._nop_callback self.callback = callback self.textbox = ReadOnlyEntry( self, textvariable=self._text_var, font=_file_field_font, cursor=utils.CURSORS['regular'], ) self.textbox.grid(row=0, column=0, sticky='ew') self.columnconfigure(0, weight=1) utils.bind_leftclick(self.textbox, self.browse) # The full location is displayed in a tooltip. add_tooltip(self.textbox, self._location) self.textbox.bind('<Configure>', self._text_configure) self.browse_btn = ttk.Button( self, text="...", width=1.5, command=self.browse, ) self.browse_btn.grid(row=0, column=1) self._text_var.set(self._truncate(loc))
def widget(self, frame) -> ttk.Entry: """Create the special textbox used to open the selector window. Use like 'selWin.widget(parent).grid(row=0, column=1)' to create and place the textbox. """ self.display = tk_tools.ReadOnlyEntry( frame, textvariable=self.disp_label, cursor=utils.CURSORS['regular'], ) utils.bind_leftclick( self.display, self.open_win, ) self.display.bind("<Key>", self.set_disp) utils.bind_rightclick( self.display, self.open_context, ) self.disp_btn = ttk.Button( self.display, text="...", width=1.5, command=self.open_win, ) self.disp_btn.pack(side=RIGHT) add_tooltip(self.display, self.description, show_when_disabled=True) self.save() return self.display
def __init__( self, master, is_dir=False, loc='', callback=None, ): """Initialise the field. - Set is_dir to true to look for directories, instead of files. - width sets the number of characters to display. - callback is a function to be called with the new path whenever it changes. """ from tooltip import add_tooltip super(FileField, self).__init__(master) self._location = loc self.is_dir = is_dir self._text_var = tk.StringVar(master=self, value='') if is_dir: self.browser = filedialog.Directory( self, initialdir=loc, ) else: self.browser = filedialog.SaveAs( self, initialdir=loc, ) if callback is not None: self.callback = callback self.textbox = ReadOnlyEntry( self, textvariable=self._text_var, font=_file_field_font, cursor=utils.CURSORS['regular'], ) self.textbox.grid(row=0, column=0, sticky='ew') self.columnconfigure(0, weight=1) utils.bind_leftclick(self.textbox, self.browse) # The full location is displayed in a tooltip. add_tooltip(self.textbox, self._location) self.textbox.bind('<Configure>', self._text_configure) self.browse_btn = ttk.Button( self, text="...", width=1.5, command=self.browse, ) self.browse_btn.grid(row=0, column=1) self._text_var.set(self._truncate(loc))
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, image=man._img_blank, ) utils.bind_leftclick(self._lbl, self._evt_start) self._lbl.bind(utils.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 utils.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, image=img.png('icons/gear'), relief='ridge', ) @utils.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. utils.bind_rightclick(self._info_btn, config_event) else: self._info_btn = None
def make_widgets(self, master: 'CheckDetails'): if self.master is not None: # If we let items move between lists, the old widgets will become # orphaned! raise ValueError( "Can't move Item objects between lists!" ) self.master = master self.check = ttk.Checkbutton( master.wid_frame, variable=self.state_var, onvalue=1, offvalue=0, takefocus=False, width=0, style='CheckDetails.TCheckbutton', command=self.master.update_allcheck, ) if self.locked: self.check.state(['disabled']) self.val_widgets = [] for value in self.values: wid = tk.Label( master.wid_frame, text=value, justify=tk.LEFT, anchor=tk.W, background='white', ) add_tooltip(wid) if self.hover_text: wid.tooltip_text = self.hover_text wid.hover_override = True else: wid.tooltip_text = '' wid.hover_override = False if not self.locked: # Allow clicking on the row to toggle the checkbox wid.bind('<Enter>', self.hover_start, add='+') wid.bind('<Leave>', self.hover_stop, add='+') utils.bind_leftclick(wid, self.row_click, add='+') wid.bind(utils.EVENTS['LEFT_RELEASE'], self.row_unclick, add='+') self.val_widgets.append(wid) utils.add_mousewheel( self.master.wid_canvas, self.check, *self.val_widgets )
def make_color_swatch(parent: tk.Frame, var: tk.StringVar, size=16) -> ttk.Label: """Make a single swatch.""" # Note: tkinter requires RGB as ints, not float! def get_color(): """Parse out the color.""" color = var.get() if color.startswith('#'): try: r = int(color[1:3], base=16) g = int(color[3:5], base=16) b = int(color[5:], base=16) except ValueError: LOGGER.warning('Invalid RGB value: "{}"!', color) r = g = b = 128 else: r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) return r, g, b def open_win(e): """Display the color selection window.""" widget_sfx() r, g, b = get_color() new_color, tk_color = askcolor( color=(r, g, b), parent=parent.winfo_toplevel(), title=_('Choose a Color'), ) if new_color is not None: r, g, b = map(int, new_color) # Returned as floats, which is wrong. var.set('{} {} {}'.format(int(r), int(g), int(b))) swatch = ttk.Label( parent, relief='raised', ) def update_image(var_name: str, var_index: str, operation: str): r, g, b = get_color() swatch['image'] = img.color_square(round(Vec(r, g, b)), size) update_image('', '', '') # Register a function to be called whenever this variable is changed. var.trace_add('write', update_image) utils.bind_leftclick(swatch, open_win) return swatch
def make_widgets(self, master: 'CheckDetails'): if self.master is not None: # If we let items move between lists, the old widgets will become # orphaned! raise ValueError("Can't move Item objects between lists!") self.master = master self.check = ttk.Checkbutton( master.wid_frame, variable=self.state_var, onvalue=1, offvalue=0, takefocus=False, width=0, style='CheckDetails.TCheckbutton', command=self.master.update_allcheck, ) if self.locked: self.check.state(['disabled']) self.val_widgets = [] for value in self.values: wid = tk.Label( master.wid_frame, text=value, justify=tk.LEFT, anchor=tk.W, background='white', ) add_tooltip(wid) if self.hover_text: set_tooltip(wid, self.hover_text) wid.hover_override = True else: set_tooltip(wid) wid.hover_override = False if not self.locked: # Allow clicking on the row to toggle the checkbox wid.bind('<Enter>', self.hover_start, add='+') wid.bind('<Leave>', self.hover_stop, add='+') utils.bind_leftclick(wid, self.row_click, add='+') wid.bind(utils.EVENTS['LEFT_RELEASE'], self.row_unclick, add='+') self.val_widgets.append(wid) utils.add_mousewheel(self.master.wid_canvas, self.check, *self.val_widgets)
def __init__(self, master, is_dir=False, loc='', width=24, callback=None): """Initialise the field. - Set is_dir to true to look for directories, instead of files. - width sets the number of characters to display. - loc is the initial value of the field. - callback is a function to be called with the new path whenever it changes. """ super(FileField, self).__init__(master) self._location = loc self.is_dir = is_dir self.width = width self._text_var = tk.StringVar(master=self, value=self._truncate(loc)) if is_dir: self.browser = filedialog.Directory( self, initialdir=loc, ) else: self.browser = filedialog.SaveAs( self, initialdir=loc, ) if callback is not None: self.callback = callback self.textbox = ReadOnlyEntry( self, textvariable=self._text_var, width=width, cursor=utils.CURSORS['regular'], ) self.textbox.grid(row=0, column=0) utils.bind_leftclick(self.textbox, self.browse) self.browse_btn = ttk.Button( self, text="...", width=1.5, command=self.browse, ) self.browse_btn.grid(row=0, column=1)
def make_headers(self): """Generate the heading widgets.""" for i, head_text in enumerate(self.headers): self.wid_head_frames[i] = header = ttk.Frame( self.wid_header, relief=tk.RAISED, ) self.wid_head_label[i] = label = ttk.Label( header, font='TkHeadingFont', text=head_text, ) self.wid_head_sort[i] = sorter = ttk.Label( header, font='TkHeadingFont', text='', ) label.grid(row=0, column=0, sticky='EW') sorter.grid(row=0, column=1, sticky='E') header.columnconfigure(0, weight=1) self.wid_header.add(header) def header_enter(_, label=label, sorter=sorter): label['background'] = 'lightblue' sorter['background'] = 'lightblue' def header_leave(_, label=label, sorter=sorter): label['background'] = '' sorter['background'] = '' header.bind('<Enter>', header_enter) header.bind('<Leave>', header_leave) utils.bind_leftclick(label, functools.partial(self.sort, i)) # Headers can't become smaller than their initial size - # The amount of space to show all the text + arrow header.update_idletasks() self.wid_header.paneconfig( header, minsize=header.winfo_reqwidth(), ) sorter['text'] = ''
def make_color_swatch(parent: tk.Frame, var: tk.StringVar, size=16) -> ttk.Label: """Make a single swatch.""" # Note: tkinter requires RGB as ints, not float! color = var.get() if color.startswith('#'): try: r, g, b = int(var[0:2], base=16), int(var[2:4], base=16), int(var[4:], base=16) except ValueError: LOGGER.warning('Invalid RGB value: "{}"!', color) r = g = b = 128 else: r, g, b = map(int, Vec.from_str(color, 128, 128, 128)) def open_win(e): """Display the color selection window.""" nonlocal r, g, b widget_sfx() new_color, tk_color = askcolor( color=(r, g, b), parent=parent.winfo_toplevel(), title=_('Choose a Color'), ) if new_color is not None: r, g, b = map(int, new_color) # Returned as floats, which is wrong. var.set('{} {} {}'.format(int(r), int(g), int(b))) swatch['image'] = img.color_square(round(Vec(r, g, b)), size) swatch = ttk.Label( parent, relief='raised', image=img.color_square(Vec(r, g, b), size), ) utils.bind_leftclick(swatch, open_win) return swatch
def __init__(self, win: 'selWin', title): self.parent = win super().__init__( win.pal_frame, ) self.sep_left = ttk.Separator(self) self.sep_left.grid(row=0, column=0, sticky=EW) self.columnconfigure(0, weight=1) self.title = ttk.Label( self, text=title, font=win.norm_font, width=len(title) + 2, anchor=CENTER, ) self.title.grid(row=0, column=1) self.sep_right = ttk.Separator(self) self.sep_right.grid(row=0, column=2, sticky=EW) self.columnconfigure(2, weight=1) self.arrow = ttk.Label( self, text=GRP_EXP, width=2, ) self.arrow.grid(row=0, column=3) self._visible = True # For the mouse events to work, we need to bind on all the children too. widgets = self.winfo_children() widgets.append(self) for wid in widgets: # type: Widget utils.bind_leftclick(wid, self.toggle) wid['cursor'] = utils.CURSORS['link'] self.bind('<Enter>', self.hover_start) self.bind('<Leave>', self.hover_end)
def __init__( self, tk, lst, has_none=True, has_def=True, has_snd_sample=False, none_desc=(('line', 'Do not add anything.'),), none_attrs: dict=utils.EmptyMapping, title='BEE2', desc='', readonly_desc='', callback: Callable[..., None]=None, callback_params=(), attributes=(), ): """Create a window object. Read from .selected_id to get the currently-chosen Item name, or None if the <none> Item is selected. Args: - tk: Must be a Toplevel window, either the tk() root or another window if needed. - lst: A list of Item objects, defining the visible items. - If has_none is True, a <none> item will be added to the beginning of the list. - If has_def is True, the 'Reset to Default' button will appear, which resets to the suggested item. - If has_snd_sample is True, a '>' button will appear next to names to play the associated audio sample for the item. - none_desc holds an optional description for the <none> Item, which can be used to describe what it results in. - title is the title of the selector window. - callback is a function to be called whenever the selected item changes. - callback_params is a list of additional values which will be passed to the callback function. The first arguement to the callback is always the selected item ID. - full_context controls if the short or long names are used for the context menu. - attributes is a list of AttrDef tuples. Each tuple should contain an ID, display text, and default value. If the values are True or False a check/cross will be displayed, otherwise they're a string. - desc is descriptive text to display on the window, and in the widget tooltip. - readonly_desc will be displayed on the widget tooltip when readonly. """ self.noneItem = Item( 'NONE', '', desc=none_desc, attributes=dict(none_attrs), ) self.noneItem.icon = img.png('BEE2/none_96') # The textbox on the parent window. self.display = None # type: tk_tools.ReadOnlyEntry # Variable associated with self.display. self.disp_label = StringVar() # The '...' button to open our window. self.disp_btn = None # type: ttk.Button # ID of the currently chosen item self.chosen_id = None # Callback function, and positional arugments to pass if callback is not None: self.callback = callback self.callback_params = list(callback_params) else: self.callback = None self.callback_params = () # Item object for the currently suggested item. self.suggested = None # Should we have the 'reset to default' button? self.has_def = has_def self.description = desc self.readonly_description = readonly_desc if has_none: self.item_list = [self.noneItem] + lst else: self.item_list = lst self.selected = self.item_list[0] # type: Item self.orig_selected = self.selected self.parent = tk self._readonly = False self.win = Toplevel(tk) self.win.withdraw() self.win.title("BEE2 - " + title) self.win.transient(master=tk) # Allow resizing in X and Y. self.win.resizable(True, True) self.win.iconbitmap('../BEE2.ico') # Run our quit command when the exit button is pressed, or Escape # on the keyboard. self.win.protocol("WM_DELETE_WINDOW", self.exit) self.win.bind("<Escape>", self.exit) # Allow navigating with arrow keys. self.win.bind("<KeyPress>", self.key_navigate) # A map from group name -> header widget self.group_widgets = {} # A map from folded name -> display name self.group_names = {} self.grouped_items = defaultdict(list) # A list of folded group names in the display order. self.group_order = [] # The maximum number of items that fits per row (set in flow_items) self.item_width = 1 if desc: self.desc_label = ttk.Label( self.win, text=desc, justify=LEFT, anchor=W, width=5, # Keep a small width, so this doesn't affect the # initial window size. ) self.desc_label.grid(row=0, column=0, sticky='EW') # PanedWindow allows resizing the two areas independently. self.pane_win = PanedWindow( self.win, orient=HORIZONTAL, sashpad=2, # Padding above/below panes sashwidth=3, # Width of border sashrelief=RAISED, # Raise the border between panes ) self.pane_win.grid(row=1, column=0, sticky="NSEW") self.win.columnconfigure(0, weight=1) self.win.rowconfigure(1, weight=1) self.wid = {} shim = ttk.Frame(self.pane_win, relief="sunken") shim.rowconfigure(0, weight=1) shim.columnconfigure(0, weight=1) # We need to use a canvas to allow scrolling. self.wid_canvas = Canvas(shim, highlightthickness=0) self.wid_canvas.grid(row=0, column=0, sticky="NSEW") # Add another frame inside to place labels on. self.pal_frame = ttk.Frame(self.wid_canvas) self.wid_canvas.create_window(1, 1, window=self.pal_frame, anchor="nw") self.wid_scroll = tk_tools.HidingScroll( shim, orient=VERTICAL, command=self.wid_canvas.yview, ) self.wid_scroll.grid(row=0, column=1, sticky="NS") self.wid_canvas['yscrollcommand'] = self.wid_scroll.set utils.add_mousewheel(self.wid_canvas, self.win) if utils.MAC: # Labelframe doesn't look good here on OSX self.sugg_lbl = ttk.Label( self.pal_frame, # Draw lines with box drawing characters text="\u250E\u2500Suggested\u2500\u2512" ) else: self.sugg_lbl = ttk.LabelFrame( self.pal_frame, text="Suggested", labelanchor=N, height=50, ) # Holds all the widgets which provide info for the current item. self.prop_frm = ttk.Frame(self.pane_win, borderwidth=4, relief='raised') self.prop_frm.columnconfigure(1, weight=1) # Border around the selected item icon. self.prop_icon_frm = ttk.Frame( self.prop_frm, borderwidth=4, relief='raised', width=ICON_SIZE, height=ICON_SIZE, ) self.prop_icon_frm.grid(row=0, column=0, columnspan=4) self.prop_icon = ttk.Label(self.prop_icon_frm) self.prop_icon.img = img.png('BEE2/blank_96') self.prop_icon['image'] = self.prop_icon.img self.prop_icon.grid(row=0, column=0) name_frame = ttk.Frame(self.prop_frm) self.prop_name = ttk.Label( name_frame, text="Item", justify=CENTER, font=("Helvetica", 12, "bold"), ) name_frame.grid(row=1, column=0, columnspan=4) name_frame.columnconfigure(0, weight=1) self.prop_name.grid(row=0, column=0) # For music items, add a '>' button to play sound samples if has_snd_sample and sound.initiallised: self.samp_button = samp_button = ttk.Button( name_frame, text=BTN_PLAY, width=1, ) samp_button.grid(row=0, column=1) add_tooltip( samp_button, "Play a sample of this item.", ) def set_samp_play(): samp_button['text'] = BTN_PLAY def set_samp_stop(): samp_button['text'] = BTN_STOP self.sampler = sound.SamplePlayer( stop_callback=set_samp_play, start_callback=set_samp_stop, ) samp_button['command'] = self.sampler.play_sample utils.bind_leftclick(self.prop_icon, self.sampler.play_sample) samp_button.state(('disabled',)) else: self.sampler = None self.prop_author = ttk.Label(self.prop_frm, text="Author") self.prop_author.grid(row=2, column=0, columnspan=4) self.prop_desc_frm = ttk.Frame(self.prop_frm, relief="sunken") self.prop_desc_frm.grid(row=4, column=0, columnspan=4, sticky="NSEW") self.prop_desc_frm.rowconfigure(0, weight=1) self.prop_desc_frm.columnconfigure(0, weight=1) self.prop_frm.rowconfigure(4, weight=1) self.prop_desc = tkRichText( self.prop_desc_frm, width=40, height=4, font="TkSmallCaptionFont", ) self.prop_desc.grid( row=0, column=0, padx=(2, 0), pady=2, sticky='NSEW', ) self.prop_scroll = tk_tools.HidingScroll( self.prop_desc_frm, orient=VERTICAL, command=self.prop_desc.yview, ) self.prop_scroll.grid( row=0, column=1, sticky="NS", padx=(0, 2), pady=2, ) self.prop_desc['yscrollcommand'] = self.prop_scroll.set ttk.Button( self.prop_frm, text="OK", command=self.save, ).grid( row=6, column=0, padx=(8, 8), ) if self.has_def: self.prop_reset = ttk.Button( self.prop_frm, text="Reset to Default", command=self.sel_suggested, ) self.prop_reset.grid( row=6, column=1, sticky='EW', ) ttk.Button( self.prop_frm, text="Cancel", command=self.exit, ).grid( row=6, column=2, padx=(8, 8), ) self.win.option_add('*tearOff', False) self.context_menu = Menu(self.win) self.norm_font = tk_font.nametofont('TkMenuFont') # Make a font for showing suggested items in the context menu self.sugg_font = self.norm_font.copy() self.sugg_font['weight'] = tk_font.BOLD # Make a font for previewing the suggested item self.mouseover_font = self.norm_font.copy() self.mouseover_font['slant'] = tk_font.ITALIC self.context_var = IntVar() # The headers for the context menu self.context_menus = {} # Sort alphabetically, prefering a sort key if present. self.item_list.sort(key=lambda it: it.sort_key or it.longName) for ind, item in enumerate(self.item_list): # type: int, Item if item == self.noneItem: item.button = ttk.Button( self.pal_frame, image=item.icon, ) item.context_lbl = '<None>' else: item.button = ttk.Button( self.pal_frame, text=item.shortName, image=item.icon, compound='top', ) group_key = item.group.casefold() self.grouped_items[group_key].append(item) if group_key not in self.group_names: # If the item is groupless, use 'Other' for the header. self.group_names[group_key] = item.group or 'Other' if not item.group: # Ungrouped items appear directly in the menu. menu = self.context_menu else: try: menu = self.context_menus[group_key] except KeyError: self.context_menus[group_key] = menu = Menu( self.context_menu, ) menu.add_radiobutton( label=item.context_lbl, command=functools.partial(self.sel_item_id, item.name), var=self.context_var, value=ind, ) item.win = self.win utils.bind_leftclick( item.button, functools.partial(self.sel_item, item), ) utils.bind_leftclick_double( item.button, self.save, ) # Convert to a normal dictionary, after adding all items. self.grouped_items = dict(self.grouped_items) # Figure out the order for the groups - alphabetical. # Note - empty string should sort to the beginning! self.group_order[:] = sorted(self.grouped_items.keys()) for index, (key, menu) in enumerate( sorted(self.context_menus.items(), key=itemgetter(0)), # We start with the ungrouped items, so increase the index # appropriately. start=len(self.grouped_items.get('', ()))): self.context_menu.add_cascade( menu=menu, label=self.group_names[key], ) # Set a custom attribute to keep track of the menu's index. menu.index = index for group_key, text in self.group_names.items(): self.group_widgets[group_key] = GroupHeader( self, text, ) self.group_widgets[group_key].should_show = True self.flow_items(None) self.wid_canvas.bind("<Configure>", self.flow_items) self.pane_win.add(shim) self.pane_win.add(self.prop_frm) # Force a minimum size for the two parts self.pane_win.paneconfigure(shim, minsize=100, stretch='always') self.prop_frm.update_idletasks() # Update reqwidth() self.pane_win.paneconfigure( self.prop_frm, minsize=200, stretch='never', ) if attributes: attr_frame = ttk.Frame(self.prop_frm) attr_frame.grid( row=5, column=0, columnspan=3, sticky=EW, ) self.attr = {} # Add in all the attribute labels for index, attr in enumerate(attributes): desc_label = ttk.Label( attr_frame, text=attr.desc, ) self.attr[attr.id] = val_label = ttk.Label( attr_frame, ) val_label.default = attr.default val_label.type = attr.type if attr.type is AttrTypes.BOOL: # It's a tick/cross label val_label['image'] = ( ICON_CHECK if attr.default else ICON_CROSS, ) elif attr.type is AttrTypes.COLOR: # A small colour swatch. val_label.configure( relief=RAISED, ) # Show the color value when hovered. add_tooltip(val_label) # Position in a 2-wide grid desc_label.grid( row=index // 2, column=(index % 2) * 2, sticky=E, ) val_label.grid( row=index // 2, column=(index % 2) * 2 + 1, sticky=W, ) else: self.attr = None
def init_widgets(): """Initiallise all the window components.""" global prop_window prop_window = Toplevel(TK_ROOT) prop_window.overrideredirect(1) prop_window.resizable(False, False) prop_window.transient(master=TK_ROOT) prop_window.attributes('-topmost', 1) prop_window.withdraw() # starts hidden f = ttk.Frame(prop_window, relief="raised", borderwidth="4") f.grid(row=0, column=0) ttk.Label( f, text="Properties:", anchor="center", ).grid( row=0, column=0, columnspan=3, sticky="EW", ) wid['name'] = ttk.Label(f, text="", anchor="center") wid['name'].grid(row=1, column=0, columnspan=3, sticky="EW") wid['ent_count'] = ttk.Label( f, text="", anchor="e", compound="left", image=png.spr('gear_ent'), ) wid['ent_count'].grid(row=0, column=2, rowspan=2, sticky=E) tooltip.add_tooltip( wid['ent_count'], 'The number of entities used for this item. The Source engine limits ' 'this to 2048 in total. This provides a guide to how many of these ' 'items can be placed in a map at once.' ) wid['author'] = ttk.Label(f, text="", anchor="center", relief="sunken") wid['author'].grid(row=2, column=0, columnspan=3, sticky="EW") sub_frame = ttk.Frame(f, borderwidth=4, relief="sunken") sub_frame.grid(column=0, columnspan=3, row=3) for i in range(5): wid['subitem', i] = ttk.Label( sub_frame, image=png.png('BEE2/alpha_64'), ) wid['subitem', i].grid(row=0, column=i) utils.bind_leftclick( wid['subitem', i], functools.partial(sub_sel, i), ) utils.bind_rightclick( wid['subitem', i], functools.partial(sub_open, i), ) wid['wip_dep'] = ttk.Label(f, text='', anchor="nw") wid['wip_dep'].grid(row=4, column=0, sticky="NW") ttk.Label(f, text="Description:", anchor="sw").grid( row=4, column=0, sticky="SW", ) spr_frame = ttk.Frame(f, borderwidth=4, relief="sunken") spr_frame.grid(column=1, columnspan=2, row=4, sticky=W) # sprites: inputs, outputs, rotation handle, occupied/embed state, # desiredFacing for spr_id in SPR: wid['sprite', spr_id] = sprite = ttk.Label( spr_frame, image=png.spr('ap_grey'), relief="raised", ) sprite.grid(row=0, column=spr_id.value) tooltip.add_tooltip(sprite) desc_frame = ttk.Frame(f, borderwidth=4, relief="sunken") desc_frame.grid(row=5, column=0, columnspan=3, sticky="EW") desc_frame.columnconfigure(0, weight=1) wid['desc'] = tkRichText(desc_frame, width=40, height=8) wid['desc'].grid(row=0, column=0, sticky="EW") desc_scroll = tk_tools.HidingScroll( desc_frame, orient=VERTICAL, command=wid['desc'].yview, ) wid['desc']['yscrollcommand'] = desc_scroll.set desc_scroll.grid(row=0, column=1, sticky="NS") def show_more_info(): url = selected_item.url if url is not None: try: webbrowser.open(url, new=OPEN_IN_TAB, autoraise=True) except webbrowser.Error: if messagebox.askyesno( icon="error", title="BEE2 - Error", message='Failed to open a web browser. Do you wish for ' 'the URL to be copied to the clipboard ' 'instead?', detail='"{!s}"'.format(url), parent=prop_window ): LOGGER.info("Saving {} to clipboard!", url) TK_ROOT.clipboard_clear() TK_ROOT.clipboard_append(url) # Either the webbrowser or the messagebox could cause the # properties to move behind the main window, so hide it # so it doesn't appear there. hide_context(None) wid['moreinfo'] = ttk.Button(f, text="More Info>>", command=show_more_info) wid['moreinfo'].grid(row=6, column=2, sticky=E) tooltip.add_tooltip(wid['moreinfo']) menu_info = Menu(wid['moreinfo']) menu_info.add_command(label='', state='disabled') def show_item_props(): snd.fx('expand') itemPropWin.show_window( selected_item.get_properties(), wid['changedefaults'], selected_sub_item.name, ) wid['changedefaults'] = ttk.Button( f, text="Change Defaults...", command=show_item_props, ) wid['changedefaults'].grid(row=6, column=1) tooltip.add_tooltip( wid['changedefaults'], 'Change the default settings for this item when placed.' ) wid['variant'] = ttk.Combobox( f, values=['VERSION'], exportselection=0, # On Mac this defaults to being way too wide! width=7 if utils.MAC else None, ) wid['variant'].state(['readonly']) # Prevent directly typing in values wid['variant'].bind('<<ComboboxSelected>>', set_item_version) wid['variant'].current(0) wid['variant'].grid(row=6, column=0, sticky=W) itemPropWin.init(hide_item_props)
def __init__( self, tk, lst, *, # Make all keyword-only for readability has_none=True, has_def=True, sound_sys: FileSystemChain = None, modal=False, # i18n: 'None' item description none_desc=_('Do not add anything.'), none_attrs=EmptyMapping, none_icon='BEE2/none_96.png', # i18n: 'None' item name. none_name=_("<None>"), title='BEE2', desc='', readonly_desc='', callback=None, callback_params=(), attributes=()): """Create a window object. Read from .selected_id to get the currently-chosen Item name, or None if the <none> Item is selected. Args: - tk: Must be a Toplevel window, either the tk() root or another window if needed. - lst: A list of Item objects, defining the visible items. - If has_none is True, a <none> item will be added to the beginning of the list. - If has_def is True, the 'Reset to Default' button will appear, which resets to the suggested item. - If snd_sample_sys is set, a '>' button will appear next to names to play the associated audio sample for the item. The value should be a FileSystem to look for samples in. - none_desc holds an optional description for the <none> Item, which can be used to describe what it results in. - none_icon allows changing the icon for the <none> Item. - none_name allows setting the name shown for the <none> Item. - title is the title of the selector window. - callback is a function to be called whenever the selected item changes. - callback_params is a list of additional values which will be passed to the callback function. The first argument to the callback is always the selected item ID. - full_context controls if the short or long names are used for the context menu. - attributes is a list of AttrDef tuples. Each tuple should contain an ID, display text, and default value. If the values are True or False a check/cross will be displayed, otherwise they're a string. - desc is descriptive text to display on the window, and in the widget tooltip. - readonly_desc will be displayed on the widget tooltip when readonly. - modal: If True, the window will block others while open. """ self.noneItem = Item( name='<NONE>', short_name='', icon=none_icon, desc=none_desc, attributes=dict(none_attrs), ) # The textbox on the parent window. self.display = None # type: tk_tools.ReadOnlyEntry # Variable associated with self.display. self.disp_label = StringVar() # The '...' button to open our window. self.disp_btn = None # type: ttk.Button # ID of the currently chosen item self.chosen_id = None # Callback function, and positional arugments to pass if callback is not None: self.callback = callback self.callback_params = list(callback_params) else: self.callback = None self.callback_params = () # Item object for the currently suggested item. self.suggested = None # Should we have the 'reset to default' button? self.has_def = has_def self.description = desc self.readonly_description = readonly_desc if has_none: self.item_list = [self.noneItem] + lst else: self.item_list = lst try: self.selected = self.item_list[0] # type: Item except IndexError: LOGGER.error('No items for window "{}"!', title) # We crash without items, forcefully add the None item in so at # least this works. self.item_list = [self.noneItem] self.selected = self.noneItem self.orig_selected = self.selected self.parent = tk self._readonly = False self.modal = modal self.win = Toplevel(tk) self.win.withdraw() self.win.title("BEE2 - " + title) self.win.transient(master=tk) # Allow resizing in X and Y. self.win.resizable(True, True) tk_tools.set_window_icon(self.win) # Run our quit command when the exit button is pressed, or Escape # on the keyboard. self.win.protocol("WM_DELETE_WINDOW", self.exit) self.win.bind("<Escape>", self.exit) # Allow navigating with arrow keys. self.win.bind("<KeyPress>", self.key_navigate) # A map from group name -> header widget self.group_widgets = {} # A map from folded name -> display name self.group_names = {} self.grouped_items = defaultdict(list) # A list of folded group names in the display order. self.group_order = [] # The maximum number of items that fits per row (set in flow_items) self.item_width = 1 if desc: self.desc_label = ttk.Label( self.win, text=desc, justify=LEFT, anchor=W, width=5, # Keep a small width, so this doesn't affect the # initial window size. ) self.desc_label.grid(row=0, column=0, sticky='EW') # PanedWindow allows resizing the two areas independently. self.pane_win = PanedWindow( self.win, orient=HORIZONTAL, sashpad=2, # Padding above/below panes sashwidth=3, # Width of border sashrelief=RAISED, # Raise the border between panes ) self.pane_win.grid(row=1, column=0, sticky="NSEW") self.win.columnconfigure(0, weight=1) self.win.rowconfigure(1, weight=1) shim = ttk.Frame(self.pane_win, relief="sunken") shim.rowconfigure(0, weight=1) shim.columnconfigure(0, weight=1) # We need to use a canvas to allow scrolling. self.wid_canvas = Canvas(shim, highlightthickness=0) self.wid_canvas.grid(row=0, column=0, sticky="NSEW") # Add another frame inside to place labels on. self.pal_frame = ttk.Frame(self.wid_canvas) self.wid_canvas.create_window(1, 1, window=self.pal_frame, anchor="nw") self.wid_scroll = tk_tools.HidingScroll( shim, orient=VERTICAL, command=self.wid_canvas.yview, ) self.wid_scroll.grid(row=0, column=1, sticky="NS") self.wid_canvas['yscrollcommand'] = self.wid_scroll.set utils.add_mousewheel(self.wid_canvas, self.win) if utils.MAC: # Labelframe doesn't look good here on OSX self.sugg_lbl = ttk.Label( self.pal_frame, # Draw lines with box drawing characters text="\u250E\u2500" + _("Suggested") + "\u2500\u2512", ) else: self.sugg_lbl = ttk.LabelFrame( self.pal_frame, text=_("Suggested"), labelanchor=N, height=50, ) # Holds all the widgets which provide info for the current item. self.prop_frm = ttk.Frame(self.pane_win, borderwidth=4, relief='raised') self.prop_frm.columnconfigure(1, weight=1) # Border around the selected item icon. width, height = img.tuple_size(ICON_SIZE_LRG) self.prop_icon_frm = ttk.Frame( self.prop_frm, borderwidth=4, relief='raised', width=width, height=height, ) self.prop_icon_frm.grid(row=0, column=0, columnspan=4) self.prop_icon = ttk.Label( self.prop_icon_frm, image=img.color_square(img.PETI_ITEM_BG, ICON_SIZE_LRG), ) self.prop_icon.grid(row=0, column=0) name_frame = ttk.Frame(self.prop_frm) self.prop_name = ttk.Label( name_frame, text="Item", justify=CENTER, font=("Helvetica", 12, "bold"), ) name_frame.grid(row=1, column=0, columnspan=4) name_frame.columnconfigure(0, weight=1) self.prop_name.grid(row=0, column=0) # For music items, add a '>' button to play sound samples if sound_sys is not None and sound.initiallised: self.samp_button = samp_button = ttk.Button( name_frame, text=BTN_PLAY, width=1, ) samp_button.grid(row=0, column=1) add_tooltip( samp_button, _("Play a sample of this item."), ) def set_samp_play(): samp_button['text'] = BTN_PLAY def set_samp_stop(): samp_button['text'] = BTN_STOP self.sampler = sound.SamplePlayer( stop_callback=set_samp_play, start_callback=set_samp_stop, system=sound_sys, ) samp_button['command'] = self.sampler.play_sample utils.bind_leftclick(self.prop_icon, self.sampler.play_sample) samp_button.state(('disabled', )) else: self.sampler = None # If we have a sound sampler, hold the system open while the window # is so it doesn't snap open/closed while finding files. self.sampler_held_open = False self.prop_author = ttk.Label(self.prop_frm, text="Author") self.prop_author.grid(row=2, column=0, columnspan=4) self.prop_desc_frm = ttk.Frame(self.prop_frm, relief="sunken") self.prop_desc_frm.grid(row=4, column=0, columnspan=4, sticky="NSEW") self.prop_desc_frm.rowconfigure(0, weight=1) self.prop_desc_frm.columnconfigure(0, weight=1) self.prop_frm.rowconfigure(4, weight=1) self.prop_desc = tkRichText( self.prop_desc_frm, width=40, height=4, font="TkSmallCaptionFont", ) self.prop_desc.grid( row=0, column=0, padx=(2, 0), pady=2, sticky='NSEW', ) self.prop_scroll = tk_tools.HidingScroll( self.prop_desc_frm, orient=VERTICAL, command=self.prop_desc.yview, ) self.prop_scroll.grid( row=0, column=1, sticky="NS", padx=(0, 2), pady=2, ) self.prop_desc['yscrollcommand'] = self.prop_scroll.set ttk.Button( self.prop_frm, text=_("OK"), command=self.save, ).grid( row=6, column=0, padx=(8, 8), ) if self.has_def: self.prop_reset = ttk.Button( self.prop_frm, text=_("Reset to Default"), command=self.sel_suggested, ) self.prop_reset.grid( row=6, column=1, sticky='EW', ) ttk.Button( self.prop_frm, text=_("Cancel"), command=self.exit, ).grid( row=6, column=2, padx=(8, 8), ) self.win.option_add('*tearOff', False) self.context_menu = Menu(self.win) self.norm_font = tk_font.nametofont('TkMenuFont') # Make a font for showing suggested items in the context menu self.sugg_font = self.norm_font.copy() self.sugg_font['weight'] = tk_font.BOLD # Make a font for previewing the suggested item self.mouseover_font = self.norm_font.copy() self.mouseover_font['slant'] = tk_font.ITALIC self.context_var = IntVar() # The headers for the context menu self.context_menus = {} # Sort alphabetically, preferring a sort key if present. self.item_list.sort(key=lambda it: it.sort_key or it.longName) for ind, item in enumerate(self.item_list): item._selector = self if item == self.noneItem: item.button = ttk.Button( self.pal_frame, image=item.icon, ) item.context_lbl = none_name else: item.button = ttk.Button( self.pal_frame, text=item.shortName, image=item.icon, compound='top', ) group_key = item.group.casefold() self.grouped_items[group_key].append(item) if group_key not in self.group_names: # If the item is groupless, use 'Other' for the header. self.group_names[group_key] = item.group or _('Other') if not item.group: # Ungrouped items appear directly in the menu. menu = self.context_menus[''] = self.context_menu else: try: menu = self.context_menus[group_key] except KeyError: self.context_menus[group_key] = menu = Menu( self.context_menu, ) menu.add_radiobutton( label=item.context_lbl, command=functools.partial(self.sel_item_id, item.name), var=self.context_var, value=ind, ) item._context_ind = len(self.grouped_items[group_key]) - 1 @utils.bind_leftclick(item.button) def click_item(event=None, *, _item=item): """Handle clicking on the item. If it's already selected, save and close the window. """ # We need to capture the item in a default, since it's # the same variable in different iterations if _item is self.selected: self.save() else: self.sel_item(_item) # Convert to a normal dictionary, after adding all items. self.grouped_items = dict(self.grouped_items) # Figure out the order for the groups - alphabetical. # Note - empty string should sort to the beginning! self.group_order[:] = sorted(self.grouped_items.keys()) for index, (key, menu) in enumerate( sorted(self.context_menus.items(), key=itemgetter(0)), # We start with the ungrouped items, so increase the index # appropriately. start=len(self.grouped_items.get('', ()))): if key == '': # Don't add the ungrouped menu to itself! continue self.context_menu.add_cascade( menu=menu, label=self.group_names[key], ) # Set a custom attribute to keep track of the menu's index. menu._context_index = index for group_key, text in self.group_names.items(): self.group_widgets[group_key] = GroupHeader( self, text, ) self.pane_win.add(shim) self.pane_win.add(self.prop_frm) # Force a minimum size for the two parts self.pane_win.paneconfigure(shim, minsize=100, stretch='always') self.prop_frm.update_idletasks() # Update reqwidth() self.pane_win.paneconfigure( self.prop_frm, minsize=200, stretch='never', ) if attributes: attr_frame = ttk.Frame(self.prop_frm) attr_frame.grid( row=5, column=0, columnspan=3, sticky=EW, ) self.attr = {} # Add in all the attribute labels for index, attr in enumerate(attributes): desc_label = ttk.Label( attr_frame, text=attr.desc, ) self.attr[attr.id] = val_label = ttk.Label(attr_frame, ) val_label.default = attr.default val_label.type = attr.type if attr.type is AttrTypes.BOOL: # It's a tick/cross label val_label['image'] = (ICON_CHECK if attr.default else ICON_CROSS, ) elif attr.type is AttrTypes.COLOR: # A small colour swatch. val_label.configure(relief=RAISED, ) # Show the color value when hovered. add_tooltip(val_label) # Position in a 2-wide grid desc_label.grid( row=index // 2, column=(index % 2) * 2, sticky=E, ) val_label.grid( row=index // 2, column=(index % 2) * 2 + 1, sticky=W, ) else: self.attr = self.desc_label = None self.flow_items() self.wid_canvas.bind("<Configure>", self.flow_items)
def init_widgets(): """Initiallise all the window components.""" global prop_window prop_window = Toplevel(TK_ROOT) prop_window.overrideredirect(1) prop_window.resizable(False, False) prop_window.transient(master=TK_ROOT) prop_window.attributes('-topmost', 1) prop_window.withdraw() # starts hidden f = ttk.Frame(prop_window, relief="raised", borderwidth="4") f.grid(row=0, column=0) ttk.Label( f, text="Properties:", anchor="center", ).grid( row=0, column=0, columnspan=3, sticky="EW", ) wid['name'] = ttk.Label(f, text="", anchor="center") wid['name'].grid(row=1, column=0, columnspan=3, sticky="EW") wid['ent_count'] = ttk.Label( f, text="", anchor="e", compound="left", image=img.spr('gear_ent'), ) wid['ent_count'].grid(row=0, column=2, rowspan=2, sticky=E) tooltip.add_tooltip( wid['ent_count'], _('The number of entities used for this item. The Source engine ' 'limits this to 2048 in total. This provides a guide to how many of ' 'these items can be placed in a map at once.')) wid['author'] = ttk.Label(f, text="", anchor="center", relief="sunken") wid['author'].grid(row=2, column=0, columnspan=3, sticky="EW") sub_frame = ttk.Frame(f, borderwidth=4, relief="sunken") sub_frame.grid(column=0, columnspan=3, row=3) for i in range(5): wid['subitem', i] = ttk.Label( sub_frame, image=img.invis_square(64), ) wid['subitem', i].grid(row=0, column=i) utils.bind_leftclick( wid['subitem', i], functools.partial(sub_sel, i), ) utils.bind_rightclick( wid['subitem', i], functools.partial(sub_open, i), ) ttk.Label(f, text=_("Description:"), anchor="sw").grid( row=4, column=0, sticky="SW", ) spr_frame = ttk.Frame(f, borderwidth=4, relief="sunken") spr_frame.grid(column=1, columnspan=2, row=4, sticky=W) # sprites: inputs, outputs, rotation handle, occupied/embed state, # desiredFacing for spr_id in SPR: wid['sprite', spr_id] = sprite = ttk.Label( spr_frame, image=img.spr('ap_grey'), relief="raised", ) sprite.grid(row=0, column=spr_id.value) tooltip.add_tooltip(sprite) desc_frame = ttk.Frame(f, borderwidth=4, relief="sunken") desc_frame.grid(row=5, column=0, columnspan=3, sticky="EW") desc_frame.columnconfigure(0, weight=1) wid['desc'] = tkRichText(desc_frame, width=40, height=16) wid['desc'].grid(row=0, column=0, sticky="EW") desc_scroll = tk_tools.HidingScroll( desc_frame, orient=VERTICAL, command=wid['desc'].yview, ) wid['desc']['yscrollcommand'] = desc_scroll.set desc_scroll.grid(row=0, column=1, sticky="NS") def show_more_info(): url = selected_item.url if url is not None: try: webbrowser.open(url, new=OPEN_IN_TAB, autoraise=True) except webbrowser.Error: if messagebox.askyesno( icon="error", title="BEE2 - Error", message=_('Failed to open a web browser. Do you wish ' 'for the URL to be copied to the clipboard ' 'instead?'), detail='"{!s}"'.format(url), parent=prop_window): LOGGER.info("Saving {} to clipboard!", url) TK_ROOT.clipboard_clear() TK_ROOT.clipboard_append(url) # Either the webbrowser or the messagebox could cause the # properties to move behind the main window, so hide it # so it doesn't appear there. hide_context(None) wid['moreinfo'] = ttk.Button(f, text=_("More Info>>"), command=show_more_info) wid['moreinfo'].grid(row=6, column=2, sticky=E) tooltip.add_tooltip(wid['moreinfo']) menu_info = Menu(wid['moreinfo']) menu_info.add_command(label='', state='disabled') def show_item_props(): snd.fx('expand') itemPropWin.show_window( selected_item.get_properties(), wid['changedefaults'], selected_sub_item.name, ) wid['changedefaults'] = ttk.Button( f, text=_("Change Defaults..."), command=show_item_props, ) wid['changedefaults'].grid(row=6, column=1) tooltip.add_tooltip( wid['changedefaults'], _('Change the default settings for this item when placed.')) wid['variant'] = ttk.Combobox( f, values=['VERSION'], exportselection=0, # On Mac this defaults to being way too wide! width=7 if utils.MAC else None, ) wid['variant'].state(['readonly']) # Prevent directly typing in values wid['variant'].bind('<<ComboboxSelected>>', set_item_version) wid['variant'].current(0) wid['variant'].grid(row=6, column=0, sticky=W) itemPropWin.init(hide_item_props)
def __init__( self, tk, lst, has_none=True, has_def=True, none_desc=(('line', 'Do not add anything.'),), title='BEE2', callback=_NO_OP, callback_params=(), attributes=(), ): """Create a window object. Read from .selected_id to get the currently-chosen Item name, or None if the <none> Item is selected. Args: - tk: Must be a Toplevel window, either the tk() root or another window if needed. - lst: A list of Item objects, defining the visible items. - If has_none is True, a <none> item will be added to the beginning of the list. - If has_def is True, the 'Reset to Default' button will appear, which resets to the suggested item. - none_desc holds an optional description for the <none> Item, which can be used to describe what it results in. - title is the title of the selector window. - callback is a function to be called whenever the selected item changes. - callback_params is a list of additional values which will be passed to the callback function. The first arguement to the callback is always the selected item ID. - full_context controls if the short or long names are used for the context menu. - attributes is a list of AttrDef tuples. Each tuple should contain an ID, display text, and default value. If the values are True or False a check/cross will be displayed, otherwise they're a string. """ self.noneItem = Item('NONE', '', desc=none_desc) self.noneItem.icon = img.png('BEE2/none_96') self.disp_label = StringVar() self.display = None self.disp_btn = None self.chosen_id = None self.callback = callback self.callback_params = callback_params self.suggested = None self.has_def = has_def if has_none: self.item_list = [self.noneItem] + lst else: self.item_list = lst self.selected = self.item_list[0] self.orig_selected = self.selected self.parent = tk self._readonly = False self.win = Toplevel(tk) self.win.withdraw() self.win.title("BEE2 - " + title) self.win.transient(master=tk) self.win.resizable(True, True) self.win.iconbitmap('../BEE2.ico') self.win.protocol("WM_DELETE_WINDOW", self.exit) self.win.bind("<Escape>", self.exit) self.win.columnconfigure(0, weight=1) self.win.rowconfigure(0, weight=1) # PanedWindow allows resizing the two areas independently. self.pane_win = PanedWindow( self.win, orient=HORIZONTAL, sashpad=2, # Padding above/below panes sashwidth=3, # Width of border sashrelief=RAISED, # Raise the border between panes ) self.pane_win.grid(row=0, column=0, sticky="NSEW") self.win.columnconfigure(0, weight=1) self.win.rowconfigure(0, weight=1) self.wid = {} shim = ttk.Frame(self.pane_win, relief="sunken") shim.rowconfigure(0, weight=1) shim.columnconfigure(0, weight=1) # We need to use a canvas to allow scrolling. self.wid_canvas = Canvas(shim, highlightthickness=0) self.wid_canvas.grid(row=0, column=0, sticky="NSEW") # Add another frame inside to place labels on. self.pal_frame = ttk.Frame(self.wid_canvas) self.wid_canvas.create_window(1, 1, window=self.pal_frame, anchor="nw") self.wid_scroll = ttk.Scrollbar( shim, orient=VERTICAL, command=self.wid_canvas.yview, ) self.wid_scroll.grid(row=0, column=1, sticky="NS") self.wid_canvas['yscrollcommand'] = self.wid_scroll.set if utils.MAC: # Labelframe doesn't look good here on OSX self.sugg_lbl = ttk.Label( self.pal_frame, # Draw lines with box drawing characters text="\u250E\u2500Suggested\u2500\u2512" ) else: self.sugg_lbl = ttk.LabelFrame( self.pal_frame, text="Suggested", labelanchor=N, height=50, ) # Holds all the widgets which provide info for the current item. self.prop_frm = ttk.Frame(self.pane_win, borderwidth=4, relief='raised') self.prop_frm.columnconfigure(1, weight=1) # Border around the selected item icon. self.prop_icon_frm = ttk.Frame( self.prop_frm, borderwidth=4, relief='raised', width=ICON_SIZE, height=ICON_SIZE, ) self.prop_icon_frm.grid(row=0, column=0, columnspan=4) self.prop_icon = ttk.Label(self.prop_icon_frm) self.prop_icon.img = img.png('BEE2/blank_96') self.prop_icon['image'] = self.prop_icon.img self.prop_icon.grid(row=0, column=0) self.prop_name = ttk.Label( self.prop_frm, text="Item", justify=CENTER, font=("Helvetica", 12, "bold"), ) self.prop_name.grid(row=1, column=0, columnspan=4) self.prop_author = ttk.Label(self.prop_frm, text="Author") self.prop_author.grid(row=2, column=0, columnspan=4) self.prop_desc_frm = ttk.Frame(self.prop_frm, relief="sunken") self.prop_desc_frm.grid(row=4, column=0, columnspan=4, sticky="NSEW") self.prop_desc_frm.rowconfigure(0, weight=1) self.prop_desc_frm.columnconfigure(0, weight=1) self.prop_frm.rowconfigure(4, weight=1) self.prop_desc = tkRichText( self.prop_desc_frm, width=40, height=4, font="TkSmallCaptionFont", ) self.prop_desc.grid( row=0, column=0, padx=(2, 0), pady=2, sticky='NSEW', ) self.prop_scroll = ttk.Scrollbar( self.prop_desc_frm, orient=VERTICAL, command=self.prop_desc.yview, ) self.prop_scroll.grid( row=0, column=1, sticky="NS", padx=(0, 2), pady=2, ) self.prop_desc['yscrollcommand'] = self.prop_scroll.set ttk.Button( self.prop_frm, text="OK", command=self.save, ).grid( row=6, column=0, padx=(8, 8), ) if self.has_def: self.prop_reset = ttk.Button( self.prop_frm, text="Reset to Default", command=self.sel_suggested, ) self.prop_reset.grid( row=6, column=1, sticky='EW', ) ttk.Button( self.prop_frm, text="Cancel", command=self.exit, ).grid( row=6, column=2, padx=(8, 8), ) self.win.option_add('*tearOff', False) self.context_menu = Menu(self.win) self.norm_font = font.nametofont('TkMenuFont') # Make a font for showing suggested items in the context menu self.sugg_font = self.norm_font.copy() self.sugg_font['weight'] = font.BOLD # Make a font for previewing the suggested item self.mouseover_font = self.norm_font.copy() self.mouseover_font['slant'] = font.ITALIC self.context_var = IntVar() # Re-order items appropriately. # self.item_list.sort( # Alphabetical order key=lambda it: it.longName ) self.item_list.sort( # Sort by group name key=lambda it: it.group.casefold() if it.group else '' ) self.item_list.sort( # Make non-grouped items go first key=lambda it: 2 if it.group else 1 ) for ind, item in enumerate(self.item_list): if item == self.noneItem: item.button = ttk.Button( self.pal_frame, image=item.icon, ) item.context_lbl = '<None>' else: item.button = ttk.Button( self.pal_frame, text=item.shortName, image=item.icon, compound='top', ) self.context_menu.add_radiobutton( label=item.context_lbl, command=functools.partial(self.sel_item_id, item.name), var=self.context_var, value=ind, ) item.win = self.win utils.bind_leftclick( item.button, functools.partial(self.sel_item, item), ) utils.bind_leftclick_double( item.button, self.save, ) self.flow_items(None) self.wid_canvas.bind("<Configure>", self.flow_items) self.pane_win.add(shim) self.pane_win.add(self.prop_frm) # Force a minimum size for the two parts self.pane_win.paneconfigure(shim, minsize=100, stretch='always') self.prop_frm.update_idletasks() # Update reqwidth() self.pane_win.paneconfigure( self.prop_frm, minsize=200, stretch='never', ) if attributes: attr_frame = ttk.Frame(self.prop_frm) attr_frame.grid( row=5, column=0, columnspan=3, sticky=EW, ) self.attr = {} # Add in all the attribute labels for index, (at_id, desc, value) in enumerate(attributes): desc_label = ttk.Label( attr_frame, text=desc, ) self.attr[at_id] = val_label = ttk.Label( attr_frame, ) val_label.default = value if isinstance(value, bool): # It's a tick/cross label val_label['image'] = ( ICON_CHECK if value else ICON_CROSS, ) # Position in a 2-wide grid desc_label.grid( row=index // 2, column=(index % 2)*2, sticky=E, ) val_label.grid( row=index // 2, column=(index % 2)*2 + 1, sticky=W, ) else: self.attr = None