예제 #1
0
파일: frame.py 프로젝트: sproaty/whyteboard
    def __init__(self):
        """
        Initialise utility, status/menu/tool bar, tabs, ctrl panel + bindings.
        """
        wx.Frame.__init__(self, None, title=_("Untitled") + u" - %s" % self.title)
        self.util = Utility(self)

        meta.find_transparent()  # important
        logger.info("Transparency supported: %s", meta.transparent)
        
        if meta.transparent:
            try:
                x = self.util.items.index(Highlighter)
            except ValueError:
                self.util.items.insert(1, Highlighter)

        self.can_paste = check_clipboard()
        self.process = None
        self.pid = None
        self.dialog = None
        self.convert_cancelled = False
        self.shape_viewer_open = False
        self.help = None
        self.hotkey_pressed = False  # for hotkey timer
        self.hotkey_timer = None
        self.tab_count = 1
        self.tab_total = 1
        self.current_tab = 0
        self.closed_tabs = []
        self.hotkeys = []

        style = (fnb.FNB_X_ON_TAB | fnb.FNB_NO_X_BUTTON | fnb.FNB_VC8 |
                 fnb.FNB_DROPDOWN_TABS_LIST | fnb.FNB_MOUSE_MIDDLE_CLOSES_TABS |
                 fnb.FNB_NO_NAV_BUTTONS)

        self.control = ControlPanel(self)
        self.tabs = fnb.FlatNotebook(self, agwStyle=style)
        self.canvas = Canvas(self.tabs, self, (Config().default_width(), Config().default_height()))
        self.panel = SidePanel(self)

        self.thumbs = self.panel.thumbs
        self.notes = self.panel.notes
        self.tabs.AddPage(self.canvas, _("Sheet") + u" 1")

        box = wx.BoxSizer(wx.HORIZONTAL)  # position windows side-by-side
        box.Add(self.control, 0, wx.EXPAND)
        box.Add(self.tabs, 1, wx.EXPAND)
        box.Add(self.panel, 0, wx.EXPAND)
        self.SetSizer(box)
        self.SetSizeWH(800, 600)

        if os.name == "posix":
            self.canvas.SetFocus()  # makes EVT_CHAR_HOOK trigger
        if 'mac' != os.name:
            self.Maximize(True)

        self.paste_check_count = PASTE_CHECK_COUNT - 1
        wx.UpdateUIEvent.SetUpdateInterval(75)
        #wx.UpdateUIEvent.SetMode(wx.UPDATE_UI_PROCESS_SPECIFIED)

        self.SetIcon(icon.getIcon())
        self.SetExtraStyle(wx.WS_EX_PROCESS_UI_UPDATES)
        self.SetDropTarget(CanvasDropTarget())
        self.statusbar = self.CreateStatusBar()
        self._print = Print(self)

        self.filehistory = wx.FileHistory(8)
        self.config = wx.Config()
        self.load_history_file()
        self.filehistory.Load(self.config)

        self.menu = Menu(self)
        self.toolbar = self.CreateToolBar() 
        Toolbar.configure(self.toolbar, self.can_paste)
        self.SetMenuBar(self.menu.menu)
        self.set_menu_from_config()
        self.do_bindings()
        self.find_help()
        
        pub.sendMessage('thumbs.update_current')
        self.update_panels(True)
        wx.CallAfter(self.UpdateWindowUI)
예제 #2
0
파일: frame.py 프로젝트: sproaty/whyteboard
class GUI(wx.Frame):
    """
    This class contains a ControlPanel, a Canvas frame and a SidePanel
    and manages their layout with a wx.BoxSizer.  A menu, toolbar and associated
    event handlers call the appropriate functions of other classes.
    """
    title = u"Whyteboard"
    LoadEvent, LOAD_DONE_EVENT = wx.lib.newevent.NewEvent()

    def __init__(self):
        """
        Initialise utility, status/menu/tool bar, tabs, ctrl panel + bindings.
        """
        wx.Frame.__init__(self, None, title=_("Untitled") + u" - %s" % self.title)
        self.util = Utility(self)

        meta.find_transparent()  # important
        logger.info("Transparency supported: %s", meta.transparent)
        
        if meta.transparent:
            try:
                x = self.util.items.index(Highlighter)
            except ValueError:
                self.util.items.insert(1, Highlighter)

        self.can_paste = check_clipboard()
        self.process = None
        self.pid = None
        self.dialog = None
        self.convert_cancelled = False
        self.shape_viewer_open = False
        self.help = None
        self.hotkey_pressed = False  # for hotkey timer
        self.hotkey_timer = None
        self.tab_count = 1
        self.tab_total = 1
        self.current_tab = 0
        self.closed_tabs = []
        self.hotkeys = []

        style = (fnb.FNB_X_ON_TAB | fnb.FNB_NO_X_BUTTON | fnb.FNB_VC8 |
                 fnb.FNB_DROPDOWN_TABS_LIST | fnb.FNB_MOUSE_MIDDLE_CLOSES_TABS |
                 fnb.FNB_NO_NAV_BUTTONS)

        self.control = ControlPanel(self)
        self.tabs = fnb.FlatNotebook(self, agwStyle=style)
        self.canvas = Canvas(self.tabs, self, (Config().default_width(), Config().default_height()))
        self.panel = SidePanel(self)

        self.thumbs = self.panel.thumbs
        self.notes = self.panel.notes
        self.tabs.AddPage(self.canvas, _("Sheet") + u" 1")

        box = wx.BoxSizer(wx.HORIZONTAL)  # position windows side-by-side
        box.Add(self.control, 0, wx.EXPAND)
        box.Add(self.tabs, 1, wx.EXPAND)
        box.Add(self.panel, 0, wx.EXPAND)
        self.SetSizer(box)
        self.SetSizeWH(800, 600)

        if os.name == "posix":
            self.canvas.SetFocus()  # makes EVT_CHAR_HOOK trigger
        if 'mac' != os.name:
            self.Maximize(True)

        self.paste_check_count = PASTE_CHECK_COUNT - 1
        wx.UpdateUIEvent.SetUpdateInterval(75)
        #wx.UpdateUIEvent.SetMode(wx.UPDATE_UI_PROCESS_SPECIFIED)

        self.SetIcon(icon.getIcon())
        self.SetExtraStyle(wx.WS_EX_PROCESS_UI_UPDATES)
        self.SetDropTarget(CanvasDropTarget())
        self.statusbar = self.CreateStatusBar()
        self._print = Print(self)

        self.filehistory = wx.FileHistory(8)
        self.config = wx.Config()
        self.load_history_file()
        self.filehistory.Load(self.config)

        self.menu = Menu(self)
        self.toolbar = self.CreateToolBar() 
        Toolbar.configure(self.toolbar, self.can_paste)
        self.SetMenuBar(self.menu.menu)
        self.set_menu_from_config()
        self.do_bindings()
        self.find_help()
        
        pub.sendMessage('thumbs.update_current')
        self.update_panels(True)
        wx.CallAfter(self.UpdateWindowUI)

    def do_bindings(self):
        """
        Performs event binding.
        """
        logger.debug("Beginning frame event bindings")
        self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CHANGED, self.on_change_tab)
        self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_CONTEXT_MENU, self.tab_popup)
        self.Bind(fnb.EVT_FLATNOTEBOOK_PAGE_DROPPED, self.on_drop_tab)
        self.Bind(self.LOAD_DONE_EVENT, self.on_done_load)
        self.Bind(wx.EVT_CHAR_HOOK, self.hotkey)
        self.Bind(wx.EVT_CLOSE, self.on_exit)
        self.Bind(wx.EVT_END_PROCESS, self.on_end_process)  # end pdf conversion
        self.menu.bindings()

        topics = {'shape.add': self.shape_add,
                  'shape.popup': self.shape_popup,
                  'shape.selected': self.shape_selected,
                  'canvas.capture_mouse': self.capture_mouse,
                  'canvas.change_tool': self.pubsub_change_tool,
                  'canvas.paste_image': self.paste_image,
                  'canvas.paste_text': self.paste_text,
                  'canvas.release_mouse': self.release_mouse,
                  'gui.mark_unsaved': self.mark_unsaved,
                  'gui.open_file': self.open_file,
                  'media.create_panel': self.make_media_panel,
                  'text.show_dialog': self.show_text_dialog}
        [pub.subscribe(value, key) for key, value in topics.items()]

        logger.debug("Setting up tool hotkeys")
        self.hotkeys = [x.hotkey for x in self.util.items]
        ac = []
        if os.name == "nt":
            for x, item in enumerate(self.util.items):
                hotkey_event = lambda evt, y = x + 1, k = item.hotkey: self.on_change_tool(evt, y, key=k)
                _id = wx.NewId()
                ac.append((wx.ACCEL_NORMAL, ord(item.hotkey.upper()), _id))
                self.Bind(wx.EVT_MENU, hotkey_event, id=_id)
        else:
            ac = [(wx.ACCEL_CTRL, ord(u'\t'), ID_NEXT),
                  (wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord(u'\t'), ID_PREV) ]

        tbl = wx.AcceleratorTable(ac)
        self.SetAcceleratorTable(tbl)


    def set_menu_from_config(self):
        """
        Sets up the program's initial menu state from the config parameters
        """
        values = {ID_TOOLBAR: u'toolbar', ID_STATUSBAR: u'statusbar',
                ID_TOOL_PREVIEW: u'tool_preview', ID_COLOUR_GRID: u'colour_grid'}
        for _id, config_key in values.items():
            if getattr(Config(), config_key)():
                self.menu.check(_id, True)
            else:
                getattr(self, u"on_" + config_key)(None, False)


    def shape_selected(self, shape):
        """
        Shape getting selected (by Select tool)
        """
        self.canvas.select_shape(shape)
        change = (shape.background == wx.TRANSPARENT)
        self.util.transparent = change
        self.control.transparent.SetValue(change)

    def make_media_panel(self, size, media):
        media.mc = MediaPanel(self.canvas, size, media)

    def release_mouse(self):
        self.canvas.release_mouse()

    def shape_popup(self, shape):
        logger.debug("Showing pop up for %s", shape)
        self.PopupMenu(ShapePopup(self.canvas, self, shape))

    def capture_mouse(self):
        self.canvas.capture_mouse()

    def shape_add(self, shape):
        self.canvas.add_shape(shape)

    def on_save(self, event=None):
        """
        Saves file if filename is set, otherwise calls 'save as'.
        """
        logger.debug("Saving file.")
        if not self.util.filename:  # no wtbd file active, prompt for location
            logger.info("Prompting for filename")
            self.on_save_as()
        else:
            self.util.save_file()


    def on_save_as(self, event=None):
        """
        Prompts for the filename and location to save to.
        """
        wildcard = _("Whyteboard file ") + u"(*.wtbd)|*.wtbd"
        _dir = Config().get('last_opened_dir') or u""
        _file = self.util.filename
        if not _file:
            _file = u""
            
        name = file_dialog(self, _("Save Whyteboard As..."),
                           wx.SAVE | wx.OVERWRITE_PROMPT, wildcard, _dir, _file)
        if name:
            if not os.path.splitext(name)[1]:  # no file extension
                name += u'.wtbd'

            if is_save_file(name):
                self.util.filename = name
                self.on_save()


    def on_open(self, event=None, text=None):
        """
        Opens a file, sets Utility's temp. file to the chosen file, prompts for
        an unsaved file and calls do_open().
        text is img/pdf/ps for the "import file" menu item
        """
        wildcard = meta.dialog_wildcard
        if text == u"img":
            wildcard = wildcard[wildcard.find(_(u"Image Files")) :
                                wildcard.find(u"|" + _(u'Whyteboard files')) ]  # image to page
        elif text:
            wildcard = wildcard[wildcard.find(u"PDF/PS/SVG") :
                                wildcard.find(u"*.SVG|")]  # page descriptions

        _dir = Config().get('last_opened_dir') or u""

        filename = file_dialog(self, _("Open file..."), wx.OPEN, wildcard, _dir)
        if filename:
            self.open_file(filename)


    def open_file(self, filename):
        if filename:
            if is_save_file(filename):
                logger.debug("File open with unsaved changes, prompting for save")
                self.prompt_for_save(self.do_open, args=[filename])
            else:
                self.do_open(filename)


    def do_open(self, filename):
        """
        Updates the appropriate variables in the utility class and loads
        the selected file.
        """
        logger.info("Opening file [%s]", filename)
        self.filehistory.AddFileToHistory(filename)
        self.filehistory.Save(self.config)
        self.config.Flush()
        self.util.save_last_path(filename)

        if is_save_file(filename):
            self.util.load_wtbd(filename)
        else:
            self.util.temp_file = filename
            self.util.load_file()


    def on_export_pdf(self, event=None):
        """
        Exports the all the sheets as a PDF. Must first export all sheets as
        imgages, convert to PDF (displaying a progress bar) and then remove
        all the temporary files
        """
        if not self.util.im_location:
            self.util.prompt_for_im()
        if not self.util.im_location:
            return
        filename = file_dialog(self, _("Export data to..."),
                               wx.SAVE | wx.OVERWRITE_PROMPT, u"PDF (*.pdf)|*.pdf")

        if filename:
            ext = os.path.splitext(filename)[1]
            if not ext:  # no file extension
                filename += u'.pdf'
            elif ext.lower() != u".pdf":
                wx.MessageBox(_("Invalid filetype to export as:") + u" .%s" % ext,
                              u"Whyteboard")
                return

            names = []
            canvas = self.canvas
            for x in range(self.tab_count):
                self.canvas = self.tabs.GetPage(x)
                name = u"%s-tempblahhahh-%s-.jpg" % (filename, x)
                names.append(name)
                self.util.export(name)
            self.canvas = canvas

            self.process = wx.Process(self)
            files = ""
            for x in names:
                files += u'"%s" ' % x  # quote filenames for windows

            cmd = u'%s -define pdf:use-trimbox=true %s"%s"' % (self.util.im_location, files, filename)
            self.pid = wx.Execute(cmd, wx.EXEC_ASYNC, self.process)
            self.show_progress_dialog(_("Converting..."), True, True)

            [os.remove(x) for x in names]



    def on_export(self, event=None):
        """Exports the current sheet as an image, or all as a PDF."""
        filename = self.export_prompt()
        if filename:
            self.util.export(filename)


    def on_export_all(self, event=None):
        """
        Iterate over the chosen filename, add a numeric value to each path to
        separate each sheet's image.
        """
        filename = self.export_prompt()
        if filename:
            name = os.path.splitext(filename)
            canvas = self.canvas
            for x in range(self.tab_count):
                self.canvas = self.tabs.GetPage(x)
                self.util.export(u"%s-%s%s" % (name[0], x + 1, name[1]))
            self.canvas = canvas


    def on_export_preferences(self, event=None):
        """
        Copies the user's preferences file to another file.
        """
        if not os.path.exists(Config().filename()):
            wx.MessageBox(_("You have not set any preferences"), _("Export Error"))
            return
        wildcard = _("Whyteboard Preference Files") + u" (*.pref)|*.pref"

        filename = file_dialog(self, _("Export preferences to..."),
                               wx.SAVE | wx.OVERWRITE_PROMPT, wildcard)
        if filename:
            if not os.path.splitext(filename)[1]:
                filename += u".pref"
            shutil.copy(os.path.join(get_home_dir(), u"user.pref"), filename)



    def update_config(self, new_config):
        old_config = Config().config
        if new_config['language'] != old_config['language']:
            wx.MessageBox(_("Whyteboard will be translated into %s when restarted")
                          % _(new_config['language']), u"Whyteboard")
            
        if 'default_font' in new_config:
            if new_config['default_font'] and not self.util.font:
                self.util.font = wx.FFont(1, wx.FONTFAMILY_DEFAULT)
                self.util.font.SetNativeFontInfoFromString(new_config['default_font'])

        # Toggles the items under "View" menu. Ignore the colour grid for now
        for menu_item in ['statusbar', 'toolbar', 'tool_preview']:
            method = getattr(self, "on_" + menu_item)
            if new_config[menu_item]:
                method(None, True)
            else:
                method(None, False)

        new_config.write()
        Config().config = new_config

        if new_config['bmp_select_transparent'] != old_config['bmp_select_transparent']:
            self.canvas.copy = None

        if not new_config['tool_preview']:
            self.control.preview.Hide()
        else:
            self.control.preview.Show()
            pub.sendMessage('gui.preview.refresh')

        wx.CallAfter(self.on_colour_grid, None, new_config['colour_grid'])

        #  too lazy to check if each colour has changed - just remake it
        self.canvas.redraw_all()
        self.control.grid.Clear(True)
        self.control.make_colour_grid()
        self.control.grid.Layout()



    def on_import_preferences(self, event=None):
        """
        Imports a preference file. Backs up the user's current preference file
        into a directory, with a timestamp on the filename
        """
        logger.debug("Prompting to import preferences")
        wildcard = _("Whyteboard Preference Files") + u" (*.pref)|*.pref"

        filename = file_dialog(self, _("Import Preferences From..."), wx.OPEN,
                               wildcard, get_home_dir())

        if filename:
            Config().init(filename)
            _dir = os.path.join(get_home_dir(), u"pref-bkup")

            if not os.path.isdir(_dir):
                os.makedirs(_dir)

            home = os.path.join(get_home_dir(), u"user.pref")
            if os.path.exists(home):
                stamp = time.strftime(u"%d-%m-%Y_%H-%M_%S")
                logger.debug("Renaming old preferences file to [%s]", stamp)
                os.rename(home, os.path.join(_dir, stamp + u".user.pref"))
                
            config = Config().clone()
            config.init(filename)
            self.update_config(config.config)
        
        
    def on_reload_preferences(self, event):
        logger.debug("Reloading preference file")
        Config().init()


    def export_prompt(self):
        """
        Find out the filename to save to
        """
        val = None  # return value
        wildcard = (u"PNG (*.png)|*.png|JPEG (*.jpg, *.jpeg)|*.jpeg;*.jpg|" +
                    u"BMP (*.bmp)|*.bmp|TIFF (*.tiff)|*.tiff")

        filename = file_dialog(self, _("Export data to..."),
                           wx.SAVE | wx.OVERWRITE_PROMPT, wildcard)
        if filename:
            _name = os.path.splitext(filename)[1].replace(u".", u"")
            types = {0: u"png", 1: u"jpg", 2: u"bmp", 3: u"tiff"}

            if not os.path.splitext(filename)[1]:
                _name = types[dlg.GetFilterIndex()]
                filename += u"." + _name
                val = filename
            if not _name in meta.types[2:]:
                wx.MessageBox(u"%s .%s" % (_("Invalid filetype to export as:"), _name),
                              u"Whyteboard")
            else:
                val = filename
        return val


    def on_new_tab(self, event=None, name=None, wb=None):
        """
        Opens a new tab, selects it, creates a new thumbnail and tree item
        name: unique name, sent by PDF convert/load file.
        wb: Passed by undo_tab to ensure the tab total is correct
        """
        if not wb:
            self.tab_total += 1
        if not name:
            name = u"%s %s" % (_("Sheet"), self.tab_total)
        logger.debug("Opening new sheet [%s]", name)

        self.tab_count += 1
        self.thumbs.new_thumb(name=name)
        self.notes.add_tab(name)
        self.tabs.AddPage(Canvas(self.tabs, self, (Config().default_width(),
                                                   Config().default_height())), name)

        self.update_panels(False)  # unhighlight current
        self.thumbs.thumbs[self.current_tab].current = True

        self.current_tab = self.tab_count - 1
        self.tabs.SetSelection(self.current_tab)  # fires on_change_tab
        self.on_change_tab()


    def on_change_tab(self, event=None):
        """Updates tab vars, scrolls thumbnails and selects tree node"""
        self.canvas = self.tabs.GetCurrentPage()
        self.update_panels(False)
        self.current_tab = self.tabs.GetSelection()

        self.update_panels(True)
        self.thumbs.thumbs[self.current_tab].update()
        self.thumbs.ScrollChildIntoView(self.thumbs.thumbs[self.current_tab])
        self.control.change_tool()  # updates canvas' shape

        if self.notes.tabs:
            tree_id = self.notes.tabs[self.current_tab]
            self.notes.tree.SelectItem(tree_id, True)
        pub.sendMessage('update_shape_viewer')



    def prompt_for_save(self, method, style=wx.YES_NO | wx.CANCEL, args=None):
        """
        Ask the user to save, quit or cancel (quitting) if they haven't saved.
        Can be called through "Update", "Open (.wtbd)", or "Exit". If updating,
        don't show a cancel button, and explicitly restart if the user cancels
        out of the "save file" dialog
        method(*args) specifies the action to perform if user selects yes or no
        """
        if not args:
            args = []

        if not self.util.saved:
            name = _("Untitled")
            if self.util.filename:
                name = os.path.basename(self.util.filename)

            dialog = PromptForSave(self, name, method, style, args)
            dialog.ShowModal()

        else:
            method(*args)
            if method == self.Destroy:
                logger.info("Program exiting.")
                sys.exit()


    def prompt_for_im(self):
        dlg = FindIM(self.util, self, self.util.check_im_path)
        dlg.ShowModal()


    def on_drop_tab(self, event):
        """
        Update the thumbs/notes so that they're poiting to the new tab position.
        Show a progress dialog, as all thumbnails must be updated.
        """
        if event.GetSelection() == event.GetOldSelection():
            return

        self.show_progress_dialog(_("Loading..."))
        self.dialog.Show()
        self.on_change_tab()

        pub.sendMessage('sheet.move', event=event, tab_count=self.tab_count)
        self.on_done_load()
        wx.MilliSleep(100)  # try and stop user dragging too many tabs quickly
        wx.SafeYield()
        pub.sendMessage('update_shape_viewer')


    def update_panels(self, select):
        """Updates thumbnail panel's text"""
        pub.sendMessage('thumbs.text.highlight', tab=self.current_tab,
                        select=select)


    def on_close_tab(self, event=None):
        """
        Closes the current tab (if there are any to close).
        """
        if self.is_only_one_tab_open():
            return
        logger.debug("Closing sheet [%s]", self.tabs.GetPageText(self.current_tab))
        
        self.tab_count -= 1
        self.notes.remove_tab(self.current_tab)
        self.thumbs.remove(self.current_tab)

        for x in self.canvas.medias:
            x.remove_panel()
        self.create_sheet_undo_point(self.canvas, self.current_tab)

        if os.name == "posix":
            self.tabs.RemovePage(self.current_tab)
        else:
            self.tabs.DeletePage(self.current_tab)

        self.on_change_tab()  # updates self.canvas


    def create_sheet_undo_point(self, canvas, tab_number, recreate_menu=True):
        """
        Creates an undo entry for a tab that's being closed
        """
        if len(self.closed_tabs) == UNDO_SHEET_COUNT:
            del self.closed_tabs[0]

        item = {'shapes': canvas.shapes,
                'undo': canvas.undo_list,
                'redo': canvas.redo_list,
                'size': canvas.area,
                'name': self.tabs.GetPageText(tab_number),
                'medias': canvas.medias,
                'viewport': canvas.GetViewStart()}

        self.closed_tabs.append(item)
        if recreate_menu:
            self.menu.make_closed_tabs_menu()


    def on_close_all_sheets(self, event=None):
        """
        Closes every sheet, creating undo points for each one.
        """
        if self.is_only_one_tab_open():
            return

        for x in reversed(range(self.tab_count)):
            self.create_sheet_undo_point(self.tabs.GetPage(x), x, False)

        self.menu.make_closed_tabs_menu()
        self.remove_all_sheets()
        self.on_new_tab()


    def on_close_other_sheets(self, event=None):
        """
        Closes all sheets except the current open sheet
        """
        if self.is_only_one_tab_open():
            return
        
        current_tab = self.current_tab
        logger.debug("Closing other sheets besides [%s]", self.tabs.GetPageText(self.current_tab))
        
        canvases = self.get_canvases()
        next_to_last_sheets = canvases[current_tab + 1 : len(canvases)]
        
        for canvas in next_to_last_sheets:
            self.current_tab += 1
            self.canvas = canvas
            self.on_close_tab()
        
        first_to_prev_sheets = self.get_canvases()[0 : -1] 
        for canvas in first_to_prev_sheets:
            self.current_tab = 0
            self.canvas = canvas
            self.on_close_tab()
        
        self.menu.make_closed_tabs_menu()
        
        
    def is_only_one_tab_open(self):    
        if not self.tab_count - 1:
            logger.debug("Cannot close sheet -- only one sheet open")
            return True
        return False
                
    def remove_all_sheets(self):
        self.canvas.shapes = []
        self.canvas.redraw_all()
        self.tabs.DeleteAllPages()
        self.thumbs.remove_all()
        self.notes.remove_all()
        self.tab_count = 0
        self.tab_total = 0


    def on_undo_tab(self, event=None, tab=None):
        """
        Undoes the last closed tab from the list.
        Re-creates the canvas from the saved shapes/undo/redo lists
        """
        if not self.closed_tabs:
            return
        if not tab:
            tab = self.closed_tabs.pop()
        else:
            tab = self.closed_tabs.pop(self.closed_tabs.index(tab))

        self.on_new_tab(name=tab['name'], wb=True)
        self.canvas.restore_sheet(tab['shapes'], tab['undo'], tab['redo'],
                                  tab['size'], tab['medias'], tab['viewport'])
        pub.sendMessage('update_shape_viewer')
        self.menu.make_closed_tabs_menu()


    def on_rename(self, event=None, sheet=None):
        if sheet is None:
            sheet = self.current_tab
        current_name = self.tabs.GetPageText(sheet)
        dlg = wx.TextEntryDialog(self, _("Rename this sheet to:"), _("Rename sheet"))
        dlg.SetValue(current_name)

        if dlg.ShowModal() == wx.ID_CANCEL:
            dlg.Destroy()
        else:
            val = dlg.GetValue()
            if val:
                logger.debug("Renaming sheet [%s] to [%s]", current_name, val)
                self.tabs.SetPageText(sheet, val)
                pub.sendMessage('sheet.rename', _id=sheet, text=val)


    def load_recent_files(self, event):
        """
        Re-creates the Recent Files menu by reloading the config file.
        """
        if self.menu.is_file_menu(event.GetMenu()):
            self.menu.remove_all_recent()
            self.load_history_file()
            self.filehistory.Load(self.config)
        event.Skip()  # otherwise interferes with EVT_UPDATE_UI


    def update_menus(self, event):
        """
        Enables/disables GUI menus and toolbar items.
        It uses a counter for the clipboard check as it can be too performance
        intense and cause segmentation faults
        """
        canvas = self.canvas
        if not canvas:
            return
        _id = event.GetId()

        if _id in [wx.ID_PASTE, ID_PASTE_NEW]:  # check this less frequently, possibly expensive
            self.paste_check_count += 1
            if self.paste_check_count == PASTE_CHECK_COUNT:
                self.can_paste = False

            if check_clipboard():
                self.can_paste = True
                self.paste_check_count = 0
                try:
                    self.menu.enable(ID_PASTE_NEW, self.can_paste)
                    self.menu.enable(wx.ID_PASTE, self.can_paste)
                except wx.PyDeadObjectError:
                    pass
            return

        if _id == ID_TRANSPARENT:
            if canvas.can_swap_transparency():
                if canvas.is_transparent():
                    event.Check(True)
                else:
                    event.Check(False)
                event.Enable(True)
            else:
                event.Enable(False)
            return

        do = False
        if _id == wx.ID_REDO and canvas.redo_list:
            do = True
        elif _id == wx.ID_UNDO and canvas.undo_list:
            do = True
        elif _id == ID_PREV and self.current_tab:
            do = True
        elif (_id == ID_NEXT and self.can_change_next_sheet()):
            do = True
        elif _id in [wx.ID_CLOSE, ID_CLOSE_ALL, ID_CLOSE_OTHERS] and self.tab_count > 1:
            do = True
        elif _id in [ID_UNDO_SHEET, ID_RECENTLY_CLOSED] and self.closed_tabs:
            do = True
        elif _id in [wx.ID_DELETE, ID_DESELECT, ID_FOREGROUND] and canvas.selected:
            do = True
        elif _id == ID_MOVE_UP and canvas.check_move(u"up"):
            do = True
        elif _id == ID_MOVE_DOWN and canvas.check_move(u"down"):
            do = True
        elif _id == ID_MOVE_TO_TOP and canvas.check_move(u"top"):
            do = True
        elif _id == ID_MOVE_TO_BOTTOM and canvas.check_move(u"bottom"):
            do = True
        elif _id in [ID_SWAP_COLOURS, ID_BACKGROUND] and canvas.can_swap_colours():
            do = True
        elif _id == wx.ID_COPY:
            if canvas:
                if canvas.copy:
                    do = True
        event.Enable(do)



    def can_change_next_sheet(self):
        return self.tab_count > 1 and self.current_tab + 1 < self.tab_count

    def on_delete_shape(self, event=None):
        self.canvas.delete_selected()

    def on_deselect_shape(self, event=None):
        self.canvas.deselect_shape()

    def on_copy(self, event):
        set_clipboard(self.canvas.get_selection_bitmap())

    def paste_image(self, bitmap, x, y, ignore=False):
        self.canvas.paste_image(bitmap, x, y, ignore)

    def paste_text(self, text, x, y):
        self.canvas.paste_text(text, x, y, self.util.colour)

    def on_paste_new(self, event):
        """ Pastes the text/image into a new tab """
        self.on_new_tab()
        self.on_paste(ignore=True)

    def on_paste(self, event=None, ignore=False):
        """
        Grabs the image from the clipboard and places it on the panel
        Ignore is used when pasting into a new sheet
        """
        data = get_clipboard()
        if not data:
            return

        x, y = 0, 0
        if not ignore:
            x, y = self.canvas.get_mouse_position()

        if isinstance(data, wx.TextDataObject):
            logger.debug("Pasting text data at coords %s", (x, y))
            self.paste_text(data.GetText(), x, y)
        else:
            logger.debug("Pasting bitmap data at coords %s", (x, y))
            self.paste_image(data.GetBitmap(), x, y, ignore)


    def on_change_tool(self, event, _id, key):
        """ Change tool -- used when being used as a hotkey """
        if self.canvas.is_not_drawing():
            self.control.change_tool(_id=_id)
            tool_name = self.canvas.shape.__class__.__name__
            logger.debug("Hotkey [%s] pressed, changing tools to [%s]", key, tool_name)

    def pubsub_change_tool(self, new=None):
        if self.canvas:
            self.change_tool(new)


    def change_tool(self, new=None, canvas=None):
        if not canvas:
            canvas = self.canvas

        self.util.change_tool(canvas, new)
        canvas.change_tool()
        pub.sendMessage('gui.preview.refresh')


    def on_fullscreen(self, event=None, val=None):
        """ Toggles fullscreen. val forces fullscreen on/off """
        flag = wx.FULLSCREEN_NOBORDER | wx.FULLSCREEN_NOCAPTION | wx.FULLSCREEN_NOSTATUSBAR
        if not val:
            val = not self.IsFullScreen()

        self.ShowFullScreen(val, flag)
        self.menu.toggle_fullscreen(val)


    def hotkey(self, event=None):
        """
        Checks for hotkeys to either change tools or to move the canvas'
        viewport. Checks for the arrow keys to move shapes about.
        """
        code = event.GetKeyCode()
        if os.name == "posix":
            for x, key in enumerate(self.hotkeys):

                if code in [ord(key), ord(key.upper())]:                    
                    self.on_change_tool(None, _id=x + 1, key=key)
                    return

        if code == wx.WXK_ESCAPE:  # close fullscreen/deselect shape
            if self.canvas.selected:
                self.canvas.deselect_shape()  # check this before fullscreen
                return
            if self.IsFullScreen():
                self.on_fullscreen(None, False)
        elif code in [wx.WXK_DOWN, wx.WXK_LEFT, wx.WXK_RIGHT, wx.WXK_UP]:
            if self.canvas.selected:
                shape = self.canvas.selected

                _map = { wx.WXK_UP: (shape.x, shape.y - SCROLL_AMOUNT),
                        wx.WXK_DOWN: (shape.x, shape.y + SCROLL_AMOUNT),
                        wx.WXK_LEFT: (shape.x - SCROLL_AMOUNT, shape.y),
                        wx.WXK_RIGHT: (shape.x + SCROLL_AMOUNT, shape.y) }

                if not self.hotkey_pressed:
                    self.hotkey_pressed = True
                    self.canvas.add_undo()
                    shape.start_select_action(0)
                    self.hotkey_timer = wx.CallLater(150, self.reset_hotkey)
                else:
                    self.hotkey_timer.Restart(150)

                shape.move(_map.get(code)[0], _map.get(code)[1],
                           offset=shape.offset(shape.x, shape.y))
                self.canvas.draw_shape(shape)
                #shape.find_edges()
                #self.canvas.shape_near_canvas_edge(shape.edges[EDGE_LEFT],
                #                                   shape.edges[EDGE_TOP], True)
                return
        self.hotkey_scroll(code, event)
        event.Skip()


    def hotkey_scroll(self, code, event):
        """Scrolls the viewport depending on the key pressed"""
        x, y = None, None
        if code == wx.WXK_HOME:
            x, y = 0, -1  # beginning of viewport
            if event.ControlDown():
                x, y = -1, 0  # top of document

        elif code == wx.WXK_END:
            x, y = self.canvas.area[0], -1  # end of viewport
            if event.ControlDown():
                x, y = -1, self.canvas.area[1]  # end of page

        elif code in [wx.WXK_PAGEUP, wx.WXK_PAGEDOWN, wx.WXK_DOWN, wx.WXK_LEFT,
                      wx.WXK_RIGHT, wx.WXK_UP]:
            x, y = self.canvas.GetViewStart()
            x2, y2 = self.canvas.GetClientSizeTuple()

            _map = { wx.WXK_PAGEUP: (-1, y - y2),
                    wx.WXK_PAGEDOWN: (-1, y + y2),
                    wx.WXK_UP: (-1, y - SCROLL_AMOUNT),
                    wx.WXK_DOWN: (-1, y + SCROLL_AMOUNT),
                    wx.WXK_LEFT: (x - SCROLL_AMOUNT, -1),
                    wx.WXK_RIGHT: (x + SCROLL_AMOUNT, -1) }

            x, y = _map.get(code)[0], _map.get(code)[1]

        if x != None and y != None:
            self.canvas.Scroll(x, y)


    def reset_hotkey(self):
        """Reset the system for the next stream of hotkey up/down events"""
        self.hotkey_pressed = False
        if not self.canvas.selected:
            return
        self.canvas.selected.end_select_action(0)
        pub.sendMessage('update_shape_viewer')


    def get_canvases(self):
        return [self.tabs.GetPage(x) for x in range(self.tab_count)]

    def get_tab_names(self):
        return [self.tabs.GetPageText(x) for x in range(self.tab_count)]

    def get_background_colour(self):
        return self.control.get_background_colour()

    def get_colour(self):
        return self.control.get_colour()

    def toggle_control(self, menu, control, force=None):
        """Menu ID to check, enable/disable; view: Control to show/hide"""
        val = self.get_toggle_value(menu, force)
        control.Show(val)
        self.menu.check(menu, val)
        self.SendSizeEvent()

    def get_toggle_value(self, menu, force):
        val = False
        if self.menu.is_checked(menu) or force:
            val = True
        if force is False:
            val = False
        return val

    def on_toolbar(self, event=None, force=None):
        self.toggle_control(ID_TOOLBAR, self.toolbar, force)

    def on_tool_preview(self, event=None, force=None):
        self.toggle_control(ID_TOOL_PREVIEW, self.control.preview, force)

    def on_statusbar(self, event=None, force=None):
        self.toggle_control(ID_STATUSBAR, self.statusbar, force)


    def on_colour_grid(self, event=None, force=None):
        val = self.get_toggle_value(ID_COLOUR_GRID, force)
        self.control.toggle_colour_grid(val)
        self.menu.check(ID_COLOUR_GRID, val)
        pub.sendMessage('gui.preview.refresh')


    def convert_dialog(self, cmd):
        """Called when the PDF convert process begins"""
        self.process = wx.Process(self)
        self.pid = wx.Execute(cmd, wx.EXEC_ASYNC, self.process)
        self.show_progress_dialog(_("Converting..."), True, True)


    def on_end_process(self, event=None):
        """ Destroy the progress process after Convert finishes """
        self.process.Destroy()
        self.dialog.Destroy()
        del self.process
        self.pid = None


    def show_text_dialog(self, text):
        dlg = TextInput(self, text=text)
        if dlg.ShowModal() == wx.ID_CANCEL:
            self.canvas.text = None
            self.canvas.redraw_all()
            self.pubsub_change_tool()
            return False

        dlg.transfer_data(self)  # grab font and text data


    def show_text_edit_dialog(self, text_shape):
        dlg = TextInput(self, text_shape)
        if dlg.ShowModal() == wx.ID_CANCEL:
            return False
        dlg.transfer_data(text_shape)  # grab font and text data
        return True


    def show_progress_dialog(self, title, cancellable=False, modal=False):
        self.dialog = ProgressDialog(self, title, cancellable)
        if modal:
            self.dialog.ShowModal()
        else:
            self.dialog.Show()


    def on_done_load(self, event=None):
        """ Refreshes thumbnails, destroys progress dialog after loading """
        self.dialog.SetTitle(_("Updating Thumbnails"))
        wx.MilliSleep(50)
        wx.SafeYield()
        self.on_refresh()  # force thumbnails
        self.dialog.Destroy()


    def on_file_history(self, evt):
        """ Handle file load from the recent files menu """
        num = evt.GetId() - wx.ID_FILE1
        path = self.filehistory.GetHistoryFile(num)
        if not os.path.exists(path):
            wx.MessageBox(_("File %s not found") % path, u"Whyteboard")
            self.filehistory.RemoveFileFromHistory(num)
            self.filehistory.Save(self.config)
            self.config.Flush()
            return

        self.filehistory.AddFileToHistory(path)  # move up the list
        self.open_file(path)


    def on_exit(self, event=None):
        logger.info("User requested application to exit.")
        self.prompt_for_save(self.Destroy)

    def tab_popup(self, event):
        self.PopupMenu(SheetsPopup(self, self, event.GetSelection()))

    def on_undo(self, event=None):
        logger.debug("Undoing last action")
        self.canvas.undo()

    def on_redo(self, event=None):
        logger.debug("Redoing last action")
        self.canvas.redo()

    def on_move_top(self, event=None):
        self.canvas.move_top(self.canvas.selected)

    def on_move_bottom(self, event=None):
        self.canvas.move_bottom(self.canvas.selected)

    def on_move_up(self, event=None):
        self.canvas.move_up(self.canvas.selected)

    def on_move_down(self, event=None):
        self.canvas.move_down(self.canvas.selected)


    def on_previous_sheet(self, event=None):
        if not self.current_tab:
            return
        self.tabs.SetSelection(self.current_tab - 1)
        self.on_change_tab()

    def on_next_sheet(self, event=None):
        if not self.can_change_next_sheet():
            return
        self.tabs.SetSelection(self.current_tab + 1)
        self.on_change_tab()


    def on_clear(self, event=None):
        self.canvas.clear(keep_images=True)

    def on_clear_all(self, event=None):
        self.canvas.clear()
        self.thumbs.update_all()

    def on_clear_sheets(self, event=None):
        for tab in range(self.tab_count):
            self.tabs.GetPage(tab).clear(keep_images=True)


    def on_clear_all_sheets(self, event=None):
        for tab in range(self.tab_count):
            self.tabs.GetPage(tab).clear()
        self.thumbs.update_all()


    def on_foreground(self, event):
        self.canvas.change_colour()

    def on_background(self, event):
        self.canvas.change_background()

    def on_refresh(self):
        self.thumbs.update_all()

    def on_transparent(self, event=None):
        self.canvas.toggle_transparent()

    def on_swap_colours(self, event=None):
        self.canvas.swap_colours()

    def on_page_setup(self, evt):
        self._print.page_setup()

    def on_print_preview(self, event):
        self._print.print_preview()

    def on_print(self, event):
        self._print.do_print()

    def on_new_win(self, event=None):
        program = (u'python', os.path.abspath(sys.argv[0]))
        if is_exe():
            program = os.path.abspath(sys.argv[0])

        logger.debug("Loading new application instance: [%s]", program)
        subprocess.Popen(program)
        

    def on_translate(self, event):
        open_url(u"https://translations.launchpad.net/whyteboard")

    def on_report_bug(self, event):
        open_url(u"https://bugs.launchpad.net/whyteboard")

    def on_resize(self, event=None):
        show_dialog(Resize(self))

    def on_preferences(self, event=None):
        show_dialog(Preferences(self))

    def on_update(self, event=None):
        show_dialog(UpdateDialog(self))

    def on_history(self, event=None):
        show_dialog(History(self))

    def on_pdf_cache(self, event=None):
        show_dialog(PDFCacheDialog(self, self.util.library))

    def on_feedback(self, event):
        show_dialog(Feedback(self), False)

    def load_history_file(self):
        self.config = wx.Config(u"Whyteboard", style=wx.CONFIG_USE_LOCAL_FILE)


    def on_shape_viewer(self, event=None):
        if not self.shape_viewer_open:
            self.shape_viewer_open = True
            show_dialog(ShapeViewer(self), False)


    def mark_unsaved(self):
        if self.util.saved:
            self.util.saved = False
            self.SetTitle(u"*" + self.GetTitle())


    def find_help(self):
        """Locate the help files, update self.help var"""
        self.help = None

        if os.path.exists(help_file_path()):
            self.help = HtmlHelpController()
            self.help.AddBook(help_file_path())


    def on_help(self, event=None, page=None):
        """
        Shows the help file, if it exists, otherwise prompts the user to
        download it.
        """
        if self.help and os.path.exists(help_file_path()):
            if page:
                self.help.Display(page)
            else:
                self.help.DisplayIndex()
        else:
            if self.download_help():
                self.on_help(page=page)


    def download_help(self):
        """Downloads the help files"""
        msg = _("Help files not found, do you want to download them?")
        d = wx.MessageDialog(self, msg, style=wx.YES_NO | wx.ICON_QUESTION)
        if d.ShowModal() == wx.ID_YES:
            try:
                download_help_files(self.util.path[0])
                self.find_help()
                return True
            except IOError:
                return False


    def on_about(self, event=None):
        inf = wx.AboutDialogInfo()
        inf.Name = u"Whyteboard"
        inf.Version = meta.version
        inf.Copyright = u"© 2009-2011 Steven Sproat"
        inf.Description = _("A simple whiteboard and PDF annotator")
        inf.Developers = [u"Steven Sproat <*****@*****.**>"]
        inf.Translators = meta.translators
        inf.WebSite = (u"http://www.whyteboard.org", u"http://www.whyteboard.org")
        inf.Licence = u"GPL 3"
        license = os.path.join(get_path(), u"LICENSE.txt")
        if os.path.exists(license):
            with open(license) as f:
                inf.Licence = f.read()

        if os.name == "nt":
            AboutDialog(self, inf)
        else:
            wx.AboutBox(inf)