Example #1
0
    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))
Example #2
0
    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
Example #3
0
    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
Example #4
0
    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))
Example #5
0
    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
Example #6
0
    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
        )
Example #7
0
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
Example #8
0
    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)
Example #9
0
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
Example #10
0
    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)
Example #11
0
    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'] = ''
Example #12
0
    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'] = ''
Example #13
0
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
Example #14
0
    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)
Example #15
0
    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)
Example #16
0
    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
Example #17
0
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)
Example #18
0
    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)
Example #19
0
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)
Example #20
0
    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