def get_icon(icon, size, err_icon): if icon is None: # Unset. return None elif icon == '<black>': return img.color_square(Vec(), size) else: return img.png( icon, error=err_icon, resize_to=size, algo=img.Image.LANCZOS, )
def get_icon(icon, size, err_icon): if icon is None: # Unset. return None elif icon == '<black>': return img.color_square(Vec(), size) else: return img.png( icon, error=err_icon, resize_to=size, algo=img.Image.LANCZOS, )
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)
def sel_item(self, item: Item, _=None): self.prop_name['text'] = item.longName if len(item.authors) == 0: self.prop_author['text'] = '' elif len(item.authors) == 1: self.prop_author['text'] = 'Author: ' + item.authors[0] else: self.prop_author['text'] = 'Authors: ' + ', '.join(item.authors) self.prop_icon['image'] = item.icon self.prop_desc.set_text(item.desc) self.selected.button.state(('!alternate',)) self.selected = item item.button.state(('alternate',)) if self.has_def: if self.suggested is None or self.selected == self.suggested: self.prop_reset.state(('disabled',)) else: self.prop_reset.state(('!disabled',)) if self.attr: # Set the attribute items. for attr_id, label in self.attr.items(): val = item.attrs.get(attr_id, label.default) if label.type is AttrTypes.BOOL: label['image'] = ( ICON_CHECK if val else ICON_CROSS ) elif label.type is AttrTypes.COLOR: label['image'] = img.color_square(val, size=16) # Display the full color when hovering.. label.tooltip_text = 'Color: R={r}, G={g}, B={b}'.format( r=int(val.x), g=int(val.y), b=int(val.z), ) elif label.type is AttrTypes.LIST: # Join the values (in alphabetical order) label['text'] = ', '.join(sorted(val)) elif label.type is AttrTypes.STRING: # Just a string. label['text'] = str(val) else: raise ValueError( 'Invalid attribute type: "{}"'.format(label.type) )
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.color_square(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, image=self._img_blank, ) drag_lbl.grid(row=0, column=0) drag_win.bind(utils.EVENTS['LEFT_RELEASE'], self._evt_stop)
def __init__( self, name: str, short_name: str, long_name: str = None, icon=None, large_icon=None, authors: list = None, desc: Union[tkMarkdown.MarkdownData, str] = '', group: str = None, sort_key: str = None, attributes: dict = None, snd_sample: str = None, ): self.name = name self.shortName = short_name self.group = group or '' self.longName = long_name or short_name self.sort_key = sort_key if len(self.longName) > 20: self.context_lbl = self.shortName else: self.context_lbl = self.longName if icon is not None: self.icon = get_icon(icon, ICON_SIZE, err_icon) else: self.icon = img.color_square(img.PETI_ITEM_BG, ICON_SIZE) self.large_icon = get_icon(large_icon, ICON_SIZE_LRG, err_icon_lrg) if isinstance(desc, str): self.desc = tkMarkdown.convert(desc) else: self.desc = desc self.snd_sample = snd_sample self.authors = authors or [] self.attrs = attributes or {} self.button = None # type: ttk.Button self.win = None # type: Toplevel self.win_x = None # type: int self.win_y = None # type: int
def __init__( self, name, short_name, long_name=None, icon=None, large_icon=None, authors=None, desc='', group='', sort_key=None, attributes=None, snd_sample=None, ): self.name = name self.shortName = short_name self.group = group or '' self.longName = long_name or short_name self.sort_key = sort_key if len(self.longName) > 20: self._context_lbl = self.shortName else: self._context_lbl = self.longName if icon is not None: self.icon = get_icon(icon, ICON_SIZE, err_icon) else: self.icon = img.color_square(img.PETI_ITEM_BG, ICON_SIZE) self.large_icon = get_icon(large_icon, ICON_SIZE_LRG, err_icon_lrg) if isinstance(desc, str): self.desc = tkMarkdown.convert(desc) else: self.desc = desc self.snd_sample = snd_sample self.authors = authors or [] self.attrs = attributes or {} self.button = None self._selector = None self._context_ind = None
def __init__( self, name, short_name, long_name=None, icon=None, large_icon=None, authors=None, desc='', group='', sort_key=None, attributes=None, snd_sample=None, ): self.name = name self.shortName = short_name self.group = group or '' self.longName = long_name or short_name self.sort_key = sort_key if len(self.longName) > 20: self._context_lbl = self.shortName else: self._context_lbl = self.longName if icon is not None: self.icon = get_icon(icon, ICON_SIZE, err_icon) else: self.icon = img.color_square(img.PETI_ITEM_BG, ICON_SIZE) self.large_icon = get_icon(large_icon, ICON_SIZE_LRG, err_icon_lrg) if isinstance(desc, str): self.desc = tkMarkdown.convert(desc) else: self.desc = desc self.snd_sample = snd_sample self.authors = authors or [] self.attrs = attributes or {} self.button = None self._selector = None self._context_ind = None
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 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)
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 sel_item(self, item: Item, event=None): self.prop_name['text'] = item.longName if len(item.authors) == 0: self.prop_author['text'] = '' else: self.prop_author['text'] = ngettext( 'Author: {}', 'Authors: {}', len(item.authors), ).format(', '.join(item.authors)) if item.large_icon is not None: # We have a large icon, use it. self.prop_icon['image'] = item.large_icon width, height = img.tuple_size(ICON_SIZE_LRG) else: # Small icon, shrink the preview. self.prop_icon['image'] = item.icon width, height = img.tuple_size(ICON_SIZE) self.prop_icon_frm.configure(width=width, height=height) self.prop_desc.set_text(item.desc) self.selected.button.state(('!alternate', )) self.selected = item item.button.state(('alternate', )) self.scroll_to(item) if self.sampler: is_playing = self.sampler.is_playing self.sampler.stop() self.sampler.cur_file = item.snd_sample if self.sampler.cur_file: self.samp_button.state(('!disabled', )) if is_playing: # Start the sampler again, so it plays the current item! self.sampler.play_sample() else: self.samp_button.state(('disabled', )) if self.has_def: if self.suggested is None or self.selected == self.suggested: self.prop_reset.state(('disabled', )) else: self.prop_reset.state(('!disabled', )) if self.attr: # Set the attribute items. for attr_id, label in self.attr.items(): val = item.attrs.get(attr_id, label.default) if label.type is AttrTypes.BOOL: label['image'] = (ICON_CHECK if val else ICON_CROSS) elif label.type is AttrTypes.COLOR: label['image'] = img.color_square(val, size=16) # Display the full color when hovering.. # i18n: Tooltip for colour swatch. set_tooltip( label, _('Color: R={r}, G={g}, B={b}').format( r=int(val.x), g=int(val.y), b=int(val.z), )) elif label.type is AttrTypes.LIST: # Join the values (in alphabetical order) label['text'] = ', '.join(sorted(val)) elif label.type is AttrTypes.STRING: # Just a string. label['text'] = str(val) else: raise ValueError('Invalid attribute type: "{}"'.format( label.type))
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)
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=2, ) 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 sel_item(self, item: Item, event=None): self.prop_name['text'] = item.longName if len(item.authors) == 0: self.prop_author['text'] = '' else: self.prop_author['text'] = ngettext( 'Author: {}', 'Authors: {}', len(item.authors), ).format( ', '.join(item.authors) ) if item.large_icon is not None: # We have a large icon, use it. self.prop_icon['image'] = item.large_icon width, height = img.tuple_size(ICON_SIZE_LRG) else: # Small icon, shrink the preview. self.prop_icon['image'] = item.icon width, height = img.tuple_size(ICON_SIZE) self.prop_icon_frm.configure(width=width, height=height) self.prop_desc.set_text(item.desc) self.selected.button.state(('!alternate',)) self.selected = item item.button.state(('alternate',)) self.scroll_to(item) if self.sampler: is_playing = self.sampler.is_playing self.sampler.stop() self.sampler.cur_file = item.snd_sample if self.sampler.cur_file: self.samp_button.state(('!disabled',)) if is_playing: # Start the sampler again, so it plays the current item! self.sampler.play_sample() else: self.samp_button.state(('disabled',)) if self.has_def: if self.suggested is None or self.selected == self.suggested: self.prop_reset.state(('disabled',)) else: self.prop_reset.state(('!disabled',)) if self.attr: # Set the attribute items. for attr_id, label in self.attr.items(): val = item.attrs.get(attr_id, label.default) if label.type is AttrTypes.BOOL: label['image'] = ( ICON_CHECK if val else ICON_CROSS ) elif label.type is AttrTypes.COLOR: label['image'] = img.color_square(val, size=16) # Display the full color when hovering.. # i18n: Tooltip for colour swatch. set_tooltip(label, _('Color: R={r}, G={g}, B={b}').format( r=int(val.x), g=int(val.y), b=int(val.z), )) elif label.type is AttrTypes.LIST: # Join the values (in alphabetical order) label['text'] = ', '.join(sorted(val)) elif label.type is AttrTypes.STRING: # Just a string. label['text'] = str(val) else: raise ValueError( 'Invalid attribute type: "{}"'.format(label.type) )