Thursday, August 23, 2012

Tkinter Dockable Toolbar Demo

This code is based on the Tcl toolbar.tcl demo. It demonstrates the creation of toolbar which can be torn-off,  and restored to, the main application window.



# File: toolbar.py
#    http://infohost.nmt.edu/tcc/help/pubs/tkinter//toplevel.html
#    http://infohost.nmt.edu/tcc/help/pubs/tkinter//layout-mgt.html#grid-methods
#    http://stackoverflow.com/questions/4055267/python-tkinter-mouse-drag-a-window-without-borders-eg-overridedirect1

from tkinter import *
from tkinter import ttk
from tkinter import font
from demopanels import MsgPanel, SeeDismissPanel

class ToolbarDemo(ttk.Frame):
    
    def __init__(self, isapp=True, name='toolbardemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Toolbar Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:    
            MsgPanel(self,
                     ["This is a demonstration of how to do ",
                      "a toolbar that is styled correctly and which ",
                      "can be torn off. The buttons are configured ",
                      "to be 'toolbar style' buttons by ",
                      "telling them that they are to use the Toolbutton ",
                      "style. At the left end of the toolbar is a ",
                      "simple marker, on mouse over, the cursor changes to a ",
                      "movement icon; drag that away from the ",
                      "toolbar to tear off the whole toolbar into a ",
                      "separate toplevel widget. When the dragged-off ",
                      "toolbar is no longer needed, just close it like ",
                      "any normal toplevel and it will reattach to the ",
                      "window it was torn from."])       
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = Frame(self)
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        sep = ttk.Separator(demoPanel, orient=HORIZONTAL)
        sep.grid(row=0, column=0, sticky='ew')
        
        # create the toolbar
        self.__tbDocked = True
        self.__tbFloating = None
        self.toolbar = self._add_toolbar(demoPanel)
        self.toolbar.grid(row=1, column=0, sticky='ew')
        
        # create a simple text area
        self.txt = Text(demoPanel, width=40, height=10)
        self.txt.grid(row=2, column=0, sticky='nsew')
        
        # position and set resize behaviour
        demoPanel.rowconfigure(2, weight=1)
        demoPanel.columnconfigure(0, weight=1)
        
    def _add_toolbar(self, parent):
        # add a toolbar (must be in a frame)
        toolbar = Frame(parent)
        
        if self.__tbDocked:
            # only create a tear off if the toolbar is being docked
            tearoff = Frame(toolbar, cursor='fleur', borderwidth=2, relief=RAISED)
            
            # use a label as a 'grip' to tearoff toolbar
            # rather than the vertical scrollbars used in the 
            # original Tcl demo
            self.__gripImg = BitmapImage(file='images\\gray25.xbm')
            grip = ttk.Label(tearoff, image=self.__gripImg)
            grip.pack(side=LEFT, fill=Y)   
            tearoff.pack(side=LEFT)
            toolbar.__tearoff = tearoff
            
            # bind the 'tearoff grip' to capture dragging
            grip.bind('<ButtonPress-1>', self._start_tear)
            grip.bind('<ButtonRelease-1>', self._tear_off)
    
        # create the toolbar widgets
        contents = ttk.Frame(toolbar)
        
        btn = ttk.Button(contents, text='Button', style='Demo.Toolbutton',
                         command=lambda: self.txt.insert(END, 'Button pressed.\n'))
        btn.pack(side=LEFT)
        
        cb = ttk.Checkbutton(contents, text='Check', style='Demo.Toolbutton')
        cb['command'] = lambda c=cb: self._say_check(c)
        cb.pack(side=LEFT)
        
        menu = Menu(contents)
        mb = ttk.Menubutton(contents, text='Menu', menu=menu)
        menu.add_command(label='Just', command=lambda: self.txt.insert(END, 'Just\n'))
        menu.add_command(label='An', command=lambda: self.txt.insert(END, 'An\n'))
        menu.add_command(label='Example', command=lambda: self.txt.insert(END, 'Example\n'))
        mb.pack(side=LEFT)
        
        combo = ttk.Combobox(contents, value=sorted(font.families()),
                             state='readonly')      
        combo.bind('<<ComboboxSelected>>', lambda e, v=combo: self._change_font(e, v.get()))  
        combo.pack(side=LEFT)
        
        contents.pack(side=LEFT)
        
        return toolbar
    
    # =========================================================================
    # Bound methods to handle 'toolbar' tear-off
    # =========================================================================
    def _start_tear(self, evt):
        # save mouse press position
        self.__tearX = evt.x
        self.__tearY = evt.y
    
    def _tear_off(self, evt):
        if self.__tbDocked:
            # undock the toolbar
            self.__tbDocked = False
            self.toolbar.grid_remove() # saves orig grid position
            
            # and create another in a new toplevel window
            tp = Toplevel()
            tp.title('Demo Toolbar')
            
            # intercept new toolbar close so we can restore
            # the original toolbar in the app window
            tp.protocol('WM_DELETE_WINDOW', self._restore_toolbar)
            
            # position the new toolbar window
            # at the mouse released position
            dx = evt.x - self.__tearX
            dy = evt.y - self.__tearY
            x = self.toolbar.winfo_rootx() + dx
            y = self.toolbar.winfo_rooty() + dy
            tp.geometry('+{}+{}'.format(x,y))

            # show the window
            self.__tbFloating = self._add_toolbar(tp)
            self.__tbFloating.pack()
            
    def _restore_toolbar(self):
        # destroy the floating toolbar
        self.__tbFloating.master.destroy()
        self.__tbFloating = None
        
        # restore the original toolbar to its
        # original position in the app window
        self.__tbDocked = True
        self.toolbar.grid()

    # =========================================================================
    # Other commands and bound methods
    # =========================================================================
        
    def _say_check(self, check):
        # triggered when the checkbutton is selected/de-selected
        state = int(check.getvar(check['variable']))
        if state == 1: 
            state = 'On' 
        else: 
            state = 'Off'
        
        msg = "Check is {}.\n".format(state)
        self.txt.insert(END, msg)
        
    def _change_font(self, evt, font ):
        # triggered when there is a selection in the Font combobox
        newFont = font.split()  # handle multi-word font names
        newFont = ''.join(newFont)  
        
        self.txt.configure(font=newFont)
        self.txt.focus_set() # pushes font update in text area
        
if __name__ == '__main__':
    ToolbarDemo().mainloop()