Example #1
0
    def __init__(self, parent, app, state = None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title = StoryFrame.DEFAULT_TITLE, \
                          size = StoryFrame.DEFAULT_SIZE)     
        self.app = app
        self.parent = parent
        self.pristine = True    # the user has not added any content to this at all
        self.dirty = False      # the user has not made unsaved changes
        self.storyFormats = {}  # list of available story formats

        # inner state
        
        if (state):
            self.buildDestination = state['buildDestination']
            self.saveDestination = state['saveDestination']
            self.target = state['target']
            self.storyPanel = StoryPanel(self, app, state = state['storyPanel'])
            self.pristine = False
        else:
            self.buildDestination = ''
            self.saveDestination = ''
            self.target = 'sugarcane'
            self.storyPanel = StoryPanel(self, app)
        
        # window events
        
        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)
        
        # Timer for the auto build file watcher
        self.autobuildtimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.autoBuildTick, self.autobuildtimer)
        
        # File menu
        
        fileMenu = wx.Menu()
        
        fileMenu.Append(wx.ID_NEW, '&New Story\tCtrl-Shift-N')
        self.Bind(wx.EVT_MENU, self.app.newStory, id = wx.ID_NEW)
        
        fileMenu.Append(wx.ID_OPEN, '&Open Story...\tCtrl-O')
        self.Bind(wx.EVT_MENU, self.app.openDialog, id = wx.ID_OPEN)
        
        recentFilesMenu = wx.Menu()
        self.recentFiles = wx.FileHistory(self.app.RECENT_FILES)
        self.recentFiles.Load(self.app.config)
        self.app.verifyRecentFiles(self)
        self.recentFiles.UseMenu(recentFilesMenu)
        self.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, 'Open &Recent', recentFilesMenu)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 0), id = wx.ID_FILE1)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 1), id = wx.ID_FILE2)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 2), id = wx.ID_FILE3)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 3), id = wx.ID_FILE4)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 4), id = wx.ID_FILE5)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 5), id = wx.ID_FILE6)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 6), id = wx.ID_FILE7)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 7), id = wx.ID_FILE8)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 8), id = wx.ID_FILE9)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 9), id = wx.ID_FILE9 + 1)
        
        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_SAVE, '&Save Story\tCtrl-S')
        self.Bind(wx.EVT_MENU, self.save, id = wx.ID_SAVE)
        
        fileMenu.Append(wx.ID_SAVEAS, 'S&ave Story As...\tCtrl-Shift-S')
        self.Bind(wx.EVT_MENU, self.saveAs, id = wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved')
        self.Bind(wx.EVT_MENU, self.revert, id = wx.ID_REVERT_TO_SAVED)
        
        fileMenu.AppendSeparator()
        
        # Import submenu
        
        importMenu = wx.Menu()

        importMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, 'Twee Source &Code...')
        self.Bind(wx.EVT_MENU, self.importSource, id = StoryFrame.FILE_IMPORT_SOURCE) 
        importMenu.Append(StoryFrame.FILE_IMPORT_HTML, 'Compiled &HTML File...')
        self.Bind(wx.EVT_MENU, self.importHtml, id = StoryFrame.FILE_IMPORT_HTML) 
        
        fileMenu.AppendMenu(wx.ID_ANY, '&Import', importMenu)
        
        # Export submenu
        
        exportMenu = wx.Menu()
        
        exportMenu.Append(StoryFrame.FILE_EXPORT_SOURCE, 'Twee Source &Code...')
        self.Bind(wx.EVT_MENU, self.exportSource, id = StoryFrame.FILE_EXPORT_SOURCE)
        
        exportMenu.Append(StoryFrame.FILE_EXPORT_PROOF, '&Proofing Copy...')
        self.Bind(wx.EVT_MENU, self.proof, id = StoryFrame.FILE_EXPORT_PROOF) 
        
        fileMenu.AppendMenu(wx.ID_ANY, '&Export', exportMenu)
        
        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_CLOSE, '&Close Story\tCtrl-W')
        self.Bind(wx.EVT_MENU, self.checkCloseMenu, id = wx.ID_CLOSE)
        
        fileMenu.Append(wx.ID_EXIT, 'E&xit Twine\tCtrl-Q')
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id = wx.ID_EXIT)
        

        
        # Edit menu
        
        editMenu = wx.Menu()
        
        editMenu.Append(wx.ID_UNDO, '&Undo\tCtrl-Z')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id = wx.ID_UNDO)
        
        if sys.platform == 'darwin':
            shortcut = 'Ctrl-Shift-Z'
        else:
            shortcut = 'Ctrl-Y'
            
        editMenu.Append(wx.ID_REDO, '&Redo\t' + shortcut)
        self.Bind(wx.EVT_MENU, lambda e: self.bodyInput.Redo(), id = wx.ID_REDO)

        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_CUT, 'Cu&t\tCtrl-X')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cutWidgets(), id = wx.ID_CUT)
        
        editMenu.Append(wx.ID_COPY, '&Copy\tCtrl-C')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.copyWidgets(), id = wx.ID_COPY)
        
        editMenu.Append(wx.ID_PASTE, '&Paste\tCtrl-V')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.pasteWidgets(), id = wx.ID_PASTE)
        
        editMenu.Append(wx.ID_DELETE, '&Delete\tDel')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, 'Select &All\tCtrl-A')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(True, exclusive = False)), id = wx.ID_SELECTALL)
        
        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_FIND, 'Find...\tCtrl-F')
        self.Bind(wx.EVT_MENU, self.showFind, id = wx.ID_FIND)

        editMenu.Append(StoryFrame.EDIT_FIND_NEXT, 'Find Next\tCtrl-G')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.findWidgetRegexp(), id = StoryFrame.EDIT_FIND_NEXT)
        
        if sys.platform == 'darwin':
            shortcut = 'Ctrl-Shift-H'
        else:
            shortcut = 'Ctrl-H'
        
        editMenu.Append(wx.ID_REPLACE, 'Replace Across Entire Story...\t' + shortcut)
        self.Bind(wx.EVT_MENU, self.showReplace, id = wx.ID_REPLACE)

        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_PREFERENCES, 'Preferences...\tCtrl-,')
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id = wx.ID_PREFERENCES)
        
        # View menu
 
        viewMenu = wx.Menu()
        
        viewMenu.Append(wx.ID_ZOOM_IN, 'Zoom &In\t=')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        viewMenu.Append(wx.ID_ZOOM_OUT, 'Zoom &Out\t-')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)
        
        viewMenu.Append(wx.ID_ZOOM_FIT, 'Zoom to &Fit\t0')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, 'Zoom &100%\t1')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom(1), id = wx.ID_ZOOM_100)
        
        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP, 'Snap to &Grid', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.toggleSnapping(), id = StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, '&Clean Up Passages')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cleanup(), id = StoryFrame.VIEW_CLEANUP)
        
        viewMenu.AppendSeparator()
        
        viewMenu.Append(StoryFrame.VIEW_TOOLBAR, '&Toolbar', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id = StoryFrame.VIEW_TOOLBAR)

        # Story menu

        self.storyMenu = wx.Menu()
        
        self.storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE, '&New Passage\tCtrl-N')
        self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id = StoryFrame.STORY_NEW_PASSAGE)
        
        self.storyMenu.Append(wx.ID_EDIT, '&Edit Passage\tCtrl-E')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e)), id = wx.ID_EDIT)

        self.storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN, '&Edit Passage Text Fullscreen\tF12')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen = True)), \
                  id = StoryFrame.STORY_EDIT_FULLSCREEN)
        
        self.storyMenu.Append(wx.ID_DELETE, '&Delete Passage')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)
 
        self.storyMenu.AppendSeparator()
        
        self.storyMenu.Append(StoryFrame.STORY_BUILD, '&Build Story...\tCtrl-B')
        self.Bind(wx.EVT_MENU, self.build, id = StoryFrame.STORY_BUILD)        
        
        self.storyMenu.Append(StoryFrame.STORY_REBUILD, '&Rebuild Story\tCtrl-R')
        self.Bind(wx.EVT_MENU, self.rebuild, id = StoryFrame.STORY_REBUILD) 

        self.storyMenu.Append(StoryFrame.STORY_VIEW_LAST, '&View Last Build\tCtrl-L')
        self.Bind(wx.EVT_MENU, self.viewBuild, id = StoryFrame.STORY_VIEW_LAST)
        
        self.autobuildmenuitem = self.storyMenu.Append(StoryFrame.STORY_AUTO_BUILD, '&Auto Build', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.autoBuild, self.autobuildmenuitem)
        self.storyMenu.Check(StoryFrame.STORY_AUTO_BUILD, False)

        self.storyMenu.AppendSeparator()

        self.storyMenu.Append(StoryFrame.STORY_STATS, 'Story &Statistics\tCtrl-I')
        self.Bind(wx.EVT_MENU, self.stats, id = StoryFrame.STORY_STATS) 

        # Story Format submenu
        
        storyFormatMenu = wx.Menu()
        storyFormatCounter = StoryFrame.STORY_FORMAT_BASE
        storyFormatPath = app.getPath() + os.sep + 'targets' + os.sep 
        for sfdir in os.listdir(storyFormatPath):
            if os.access(storyFormatPath + sfdir + os.sep + 'header.html', os.R_OK):
                if sfdir == 'jonah':
                    sfdirlabel = 'Jonah'
                elif sfdir == 'sugarcane': 
                    sfdirlabel = 'Sugarcane'
                else: 
                    sfdirlabel = sfdir.capitalize()
                storyFormatMenu.Append(storyFormatCounter, sfdirlabel, kind = wx.ITEM_CHECK)
                self.Bind(wx.EVT_MENU, lambda e,target=sfdir: self.setTarget(target), id = storyFormatCounter)
                self.storyFormats[storyFormatCounter] = sfdir
                storyFormatCounter = storyFormatCounter + 1
        
        storyFormatMenu.AppendSeparator()
       
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP, '&About Story Formats')        
        self.Bind(wx.EVT_MENU, lambda e: self.app.storyFormatHelp(), id = StoryFrame.STORY_FORMAT_HELP)
        
        self.storyMenu.AppendMenu(wx.ID_ANY, 'Story &Format', storyFormatMenu)
        
        # Help menu
        
        helpMenu = wx.Menu()
 
        helpMenu.Append(StoryFrame.HELP_MANUAL, 'Online &Help')
        self.Bind(wx.EVT_MENU, self.app.openDocs, id = StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, '&Discuss Twine Online')
        self.Bind(wx.EVT_MENU, self.app.openGroup, id = StoryFrame.HELP_GROUP)
        
        helpMenu.Append(StoryFrame.HELP_GITHUB, 'Twine on &GitHub')
        self.Bind(wx.EVT_MENU, self.app.openGitHub, id = StoryFrame.HELP_GITHUB)
        
        helpMenu.AppendSeparator()
        
        helpMenu.Append(wx.ID_ABOUT, '&About Twine')
        self.Bind(wx.EVT_MENU, self.app.about, id = wx.ID_ABOUT)
        
        # add menus
        
        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, '&File')
        self.menus.Append(editMenu, '&Edit')
        self.menus.Append(viewMenu, '&View')
        self.menus.Append(self.storyMenu, '&Story')
        self.menus.Append(helpMenu, '&Help')
        self.SetMenuBar(self.menus)
        
        # extra shortcuts
        
        self.SetAcceleratorTable(wx.AcceleratorTable([ \
                                    (wx.ACCEL_NORMAL, wx.WXK_RETURN, wx.ID_EDIT), \
                                    (wx.ACCEL_CTRL, wx.WXK_RETURN, StoryFrame.STORY_EDIT_FULLSCREEN) \
                                                      ]))

        # add toolbar
        if sys.platform == 'darwin':
            iconPath = re.sub('lib/.*', '', os.path.realpath(sys.path[0])) + "icons" + os.sep
        else:
            iconPath = self.app.getPath() + os.sep + 'icons' + os.sep
        
        self.toolbar = self.CreateToolBar(style = wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize((StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))
        
        self.toolbar.AddLabelTool(StoryFrame.STORY_NEW_PASSAGE, 'New Passage', \
                                  wx.Bitmap(iconPath + 'newpassage.png'), \
                                  shortHelp = StoryFrame.NEW_PASSAGE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.newWidget(), id = StoryFrame.STORY_NEW_PASSAGE)
        
        self.toolbar.AddSeparator()
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_IN, 'Zoom In', \
                                  wx.Bitmap(iconPath + 'zoomin.png'), \
                                  shortHelp = StoryFrame.ZOOM_IN_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_OUT, 'Zoom Out', \
                                  wx.Bitmap(iconPath + 'zoomout.png'), \
                                  shortHelp = StoryFrame.ZOOM_OUT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)  
          
        self.toolbar.AddLabelTool(wx.ID_ZOOM_FIT, 'Zoom to Fit', \
                                  wx.Bitmap(iconPath + 'zoomfit.png'), \
                                  shortHelp = StoryFrame.ZOOM_FIT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_100, 'Zoom to 100%', \
                                  wx.Bitmap(iconPath + 'zoom1.png'), \
                                  shortHelp = StoryFrame.ZOOM_ONE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom(1.0), id = wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)
        
        if app.config.ReadBool('storyFrameToolbar'):
            self.showToolbar = True
            self.toolbar.Realize()
        else:
            self.showToolbar = False
            self.toolbar.Realize()
            self.toolbar.Hide()
Example #2
0
    def __init__(self, parent, app, state = None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title = StoryFrame.DEFAULT_TITLE, \
                          size = StoryFrame.DEFAULT_SIZE)     
        self.app = app
        self.parent = parent
        self.pristine = True    # the user has not added any content to this at all
        self.dirty = False      # the user has not made unsaved changes

        # inner state
        
        if (state):
            self.buildDestination = state['buildDestination']
            self.saveDestination = state['saveDestination']
            self.target = state['target']
            self.storyPanel = StoryPanel(self, app, state = state['storyPanel'])
            self.pristine = False
        else:
            self.buildDestination = ''
            self.saveDestination = ''
            self.target = 'sugarcane'
            self.storyPanel = StoryPanel(self, app)
        
        # window events
        
        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)
        
        # File menu
        
        fileMenu = wx.Menu()
        
        fileMenu.Append(wx.ID_NEW, '&New Story\tCtrl-Shift-N')
        self.Bind(wx.EVT_MENU, self.app.newStory, id = wx.ID_NEW)
        
        fileMenu.Append(wx.ID_OPEN, '&Open Story...\tCtrl-O')
        self.Bind(wx.EVT_MENU, self.app.openDialog, id = wx.ID_OPEN)
        
        recentFilesMenu = wx.Menu()
        self.app.recentFiles.UseMenu(recentFilesMenu)
        self.app.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, 'Open &Recent', recentFilesMenu)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(0), id = wx.ID_FILE1)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(1), id = wx.ID_FILE2)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(2), id = wx.ID_FILE3)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(3), id = wx.ID_FILE4)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(4), id = wx.ID_FILE5)
        
        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_SAVE, '&Save Story\tCtrl-S')
        self.Bind(wx.EVT_MENU, self.save, id = wx.ID_SAVE)
        
        fileMenu.Append(wx.ID_SAVEAS, 'S&ave Story As...\tCtrl-Shift-S')
        self.Bind(wx.EVT_MENU, self.saveAs, id = wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved')
        self.Bind(wx.EVT_MENU, self.revert, id = wx.ID_REVERT_TO_SAVED)
        
        fileMenu.AppendSeparator()

        fileMenu.Append(StoryFrame.FILE_EXPORT_PROOF, 'Export &Proofing Copy...')
        self.Bind(wx.EVT_MENU, self.proof, id = StoryFrame.FILE_EXPORT_PROOF) 

        fileMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, '&Import Source Code...')
        self.Bind(wx.EVT_MENU, self.importSource, id = StoryFrame.FILE_IMPORT_SOURCE) 

        fileMenu.Append(StoryFrame.FILE_EXPORT_SOURCE, 'Export Source &Code...')
        self.Bind(wx.EVT_MENU, self.exportSource, id = StoryFrame.FILE_EXPORT_SOURCE)

        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_CLOSE, '&Close Story\tCtrl-W')
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id = wx.ID_CLOSE)
        
        fileMenu.Append(wx.ID_EXIT, 'E&xit Twine\tCtrl-Q')
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id = wx.ID_EXIT)
        
        # Edit menu
        
        editMenu = wx.Menu()
        
        editMenu.Append(wx.ID_UNDO, '&Undo\tCtrl-Z')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id = wx.ID_UNDO)
        
        editMenu.Append(wx.ID_REDO, '&Redo\tCtrl-Y')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.redo(), id = wx.ID_REDO)

        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_CUT, 'Cu&t\tCtrl-X')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cutWidgets(), id = wx.ID_CUT)
        
        editMenu.Append(wx.ID_COPY, '&Copy\tCtrl-C')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.copyWidgets(), id = wx.ID_COPY)
        
        editMenu.Append(wx.ID_PASTE, '&Paste\tCtrl-V')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.pasteWidgets(), id = wx.ID_PASTE)
        
        editMenu.Append(wx.ID_DELETE, '&Delete\tDel')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, 'Select &All\tCtrl-A')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(True, exclusive = False)), id = wx.ID_SELECTALL)
        
        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_PREFERENCES, 'Preferences...\tCtrl-,')
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id = wx.ID_PREFERENCES)
        
        # View menu
 
        viewMenu = wx.Menu()
        
        viewMenu.Append(wx.ID_ZOOM_IN, 'Zoom &In\t=')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        viewMenu.Append(wx.ID_ZOOM_OUT, 'Zoom &Out\t-')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)
        
        viewMenu.Append(wx.ID_ZOOM_FIT, 'Zoom to &Fit\t0')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, 'Zoom &100%\t1')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom(1), id = wx.ID_ZOOM_100)
        
        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP, 'Snap to &Grid', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.toggleSnapping(), id = StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, '&Clean Up Passages')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cleanup(), id = StoryFrame.VIEW_CLEANUP)
        
        viewMenu.AppendSeparator()
        
        viewMenu.Append(StoryFrame.VIEW_TOOLBAR, '&Toolbar', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id = StoryFrame.VIEW_TOOLBAR)

        # Story menu

        storyMenu = wx.Menu()
        
        storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE, '&New Passage\tCtrl-N')
        self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id = StoryFrame.STORY_NEW_PASSAGE)
        
        storyMenu.Append(wx.ID_EDIT, '&Edit Passage\tCtrl-E')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e)), id = wx.ID_EDIT)

        storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN, '&Edit Passage Text Fullscreen\tF12')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen = True)), \
                  id = StoryFrame.STORY_EDIT_FULLSCREEN)
        
        storyMenu.Append(wx.ID_DELETE, '&Delete Passage')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)
 
        storyMenu.AppendSeparator()
        
        storyMenu.Append(StoryFrame.STORY_BUILD, '&Build Story...\tCtrl-B')
        self.Bind(wx.EVT_MENU, self.build, id = StoryFrame.STORY_BUILD)        
        
        storyMenu.Append(StoryFrame.STORY_REBUILD, '&Rebuild Story\tCtrl-R')
        self.Bind(wx.EVT_MENU, self.rebuild, id = StoryFrame.STORY_REBUILD) 

        storyMenu.Append(StoryFrame.STORY_VIEW_LAST, '&View Last Build\tCtrl-L')
        self.Bind(wx.EVT_MENU, self.viewBuild, id = StoryFrame.STORY_VIEW_LAST)

        storyMenu.AppendSeparator()

        storyMenu.Append(StoryFrame.STORY_STATS, 'Story &Statistics\tCtrl-I')
        self.Bind(wx.EVT_MENU, self.stats, id = StoryFrame.STORY_STATS) 

        # Story Format submenu
        
        storyFormatMenu = wx.Menu()
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_SUGARCANE, '&Sugarcane', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('sugarcane'), id = StoryFrame.STORY_FORMAT_SUGARCANE) 
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_JONAH, '&Jonah', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('jonah'), id = StoryFrame.STORY_FORMAT_JONAH) 
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW2, 'TiddlyWiki &2', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('tw2'), id = StoryFrame.STORY_FORMAT_TW2) 
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW1, 'TiddlyWiki &1', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('tw'), id = StoryFrame.STORY_FORMAT_TW1) 
        
        storyFormatMenu.AppendSeparator()
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP, '&About Story Formats')        
        self.Bind(wx.EVT_MENU, lambda e: self.app.storyFormatHelp(), id = StoryFrame.STORY_FORMAT_HELP)
        
        storyMenu.AppendMenu(wx.ID_ANY, 'Story &Format', storyFormatMenu)
        
        # Help menu
        
        helpMenu = wx.Menu()
 
        helpMenu.Append(StoryFrame.HELP_MANUAL, 'Online &Help')
        self.Bind(wx.EVT_MENU, self.app.openDocs, id = StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, '&Discuss Twine Online')
        self.Bind(wx.EVT_MENU, self.app.openGroup, id = StoryFrame.HELP_GROUP)

        helpMenu.Append(StoryFrame.HELP_BUG, 'Report a &Bug')
        self.Bind(wx.EVT_MENU, self.app.reportBug, id = StoryFrame.HELP_BUG)
        
        helpMenu.AppendSeparator()
        
        helpMenu.Append(wx.ID_ABOUT, '&About Twine')
        self.Bind(wx.EVT_MENU, self.app.about, id = wx.ID_ABOUT)
        
        # add menus
        
        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, '&File')
        self.menus.Append(editMenu, '&Edit')
        self.menus.Append(viewMenu, '&View')
        self.menus.Append(storyMenu, '&Story')
        self.menus.Append(helpMenu, '&Help')
        self.SetMenuBar(self.menus)

        # add toolbar

        iconPath = self.app.getPath() + os.sep + 'icons' + os.sep
        
        self.toolbar = self.CreateToolBar(style = wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize((StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))
        
        self.toolbar.AddLabelTool(StoryFrame.STORY_NEW_PASSAGE, 'New Passage', \
                                  wx.Bitmap(iconPath + 'newpassage.png'), \
                                  shortHelp = StoryFrame.NEW_PASSAGE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.newWidget(), id = StoryFrame.STORY_NEW_PASSAGE)
        
        self.toolbar.AddSeparator()
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_IN, 'Zoom In', \
                                  wx.Bitmap(iconPath + 'zoomin.png'), \
                                  shortHelp = StoryFrame.ZOOM_IN_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_OUT, 'Zoom Out', \
                                  wx.Bitmap(iconPath + 'zoomout.png'), \
                                  shortHelp = StoryFrame.ZOOM_OUT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)  
          
        self.toolbar.AddLabelTool(wx.ID_ZOOM_FIT, 'Zoom to Fit', \
                                  wx.Bitmap(iconPath + 'zoomfit.png'), \
                                  shortHelp = StoryFrame.ZOOM_FIT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_100, 'Zoom to 100%', \
                                  wx.Bitmap(iconPath + 'zoom1.png'), \
                                  shortHelp = StoryFrame.ZOOM_ONE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom(1.0), id = wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)
        self.showToolbar = True
        self.toolbar.Realize()
        self.Show(True)
Example #3
0
class StoryFrame (wx.Frame):
    
    def __init__(self, parent, app, state = None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title = StoryFrame.DEFAULT_TITLE, \
                          size = StoryFrame.DEFAULT_SIZE)     
        self.app = app
        self.parent = parent
        self.pristine = True    # the user has not added any content to this at all
        self.dirty = False      # the user has not made unsaved changes
        self.storyFormats = {}  # list of available story formats

        # inner state
        
        if (state):
            self.buildDestination = state['buildDestination']
            self.saveDestination = state['saveDestination']
            self.target = state['target']
            self.storyPanel = StoryPanel(self, app, state = state['storyPanel'])
            self.pristine = False
        else:
            self.buildDestination = ''
            self.saveDestination = ''
            self.target = 'sugarcane'
            self.storyPanel = StoryPanel(self, app)
        
        # window events
        
        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)
        
        # Timer for the auto build file watcher
        self.autobuildtimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.autoBuildTick, self.autobuildtimer)
        
        # File menu
        
        fileMenu = wx.Menu()
        
        fileMenu.Append(wx.ID_NEW, '&New Story\tCtrl-Shift-N')
        self.Bind(wx.EVT_MENU, self.app.newStory, id = wx.ID_NEW)
        
        fileMenu.Append(wx.ID_OPEN, '&Open Story...\tCtrl-O')
        self.Bind(wx.EVT_MENU, self.app.openDialog, id = wx.ID_OPEN)
        
        recentFilesMenu = wx.Menu()
        self.recentFiles = wx.FileHistory(self.app.RECENT_FILES)
        self.recentFiles.Load(self.app.config)
        self.app.verifyRecentFiles(self)
        self.recentFiles.UseMenu(recentFilesMenu)
        self.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, 'Open &Recent', recentFilesMenu)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 0), id = wx.ID_FILE1)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 1), id = wx.ID_FILE2)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 2), id = wx.ID_FILE3)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 3), id = wx.ID_FILE4)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 4), id = wx.ID_FILE5)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 5), id = wx.ID_FILE6)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 6), id = wx.ID_FILE7)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 7), id = wx.ID_FILE8)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 8), id = wx.ID_FILE9)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(self, 9), id = wx.ID_FILE9 + 1)
        
        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_SAVE, '&Save Story\tCtrl-S')
        self.Bind(wx.EVT_MENU, self.save, id = wx.ID_SAVE)
        
        fileMenu.Append(wx.ID_SAVEAS, 'S&ave Story As...\tCtrl-Shift-S')
        self.Bind(wx.EVT_MENU, self.saveAs, id = wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved')
        self.Bind(wx.EVT_MENU, self.revert, id = wx.ID_REVERT_TO_SAVED)
        
        fileMenu.AppendSeparator()
        
        # Import submenu
        
        importMenu = wx.Menu()

        importMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, 'Twee Source &Code...')
        self.Bind(wx.EVT_MENU, self.importSource, id = StoryFrame.FILE_IMPORT_SOURCE) 
        importMenu.Append(StoryFrame.FILE_IMPORT_HTML, 'Compiled &HTML File...')
        self.Bind(wx.EVT_MENU, self.importHtml, id = StoryFrame.FILE_IMPORT_HTML) 
        
        fileMenu.AppendMenu(wx.ID_ANY, '&Import', importMenu)
        
        # Export submenu
        
        exportMenu = wx.Menu()
        
        exportMenu.Append(StoryFrame.FILE_EXPORT_SOURCE, 'Twee Source &Code...')
        self.Bind(wx.EVT_MENU, self.exportSource, id = StoryFrame.FILE_EXPORT_SOURCE)
        
        exportMenu.Append(StoryFrame.FILE_EXPORT_PROOF, '&Proofing Copy...')
        self.Bind(wx.EVT_MENU, self.proof, id = StoryFrame.FILE_EXPORT_PROOF) 
        
        fileMenu.AppendMenu(wx.ID_ANY, '&Export', exportMenu)
        
        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_CLOSE, '&Close Story\tCtrl-W')
        self.Bind(wx.EVT_MENU, self.checkCloseMenu, id = wx.ID_CLOSE)
        
        fileMenu.Append(wx.ID_EXIT, 'E&xit Twine\tCtrl-Q')
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id = wx.ID_EXIT)
        

        
        # Edit menu
        
        editMenu = wx.Menu()
        
        editMenu.Append(wx.ID_UNDO, '&Undo\tCtrl-Z')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id = wx.ID_UNDO)
        
        if sys.platform == 'darwin':
            shortcut = 'Ctrl-Shift-Z'
        else:
            shortcut = 'Ctrl-Y'
            
        editMenu.Append(wx.ID_REDO, '&Redo\t' + shortcut)
        self.Bind(wx.EVT_MENU, lambda e: self.bodyInput.Redo(), id = wx.ID_REDO)

        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_CUT, 'Cu&t\tCtrl-X')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cutWidgets(), id = wx.ID_CUT)
        
        editMenu.Append(wx.ID_COPY, '&Copy\tCtrl-C')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.copyWidgets(), id = wx.ID_COPY)
        
        editMenu.Append(wx.ID_PASTE, '&Paste\tCtrl-V')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.pasteWidgets(), id = wx.ID_PASTE)
        
        editMenu.Append(wx.ID_DELETE, '&Delete\tDel')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, 'Select &All\tCtrl-A')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(True, exclusive = False)), id = wx.ID_SELECTALL)
        
        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_FIND, 'Find...\tCtrl-F')
        self.Bind(wx.EVT_MENU, self.showFind, id = wx.ID_FIND)

        editMenu.Append(StoryFrame.EDIT_FIND_NEXT, 'Find Next\tCtrl-G')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.findWidgetRegexp(), id = StoryFrame.EDIT_FIND_NEXT)
        
        if sys.platform == 'darwin':
            shortcut = 'Ctrl-Shift-H'
        else:
            shortcut = 'Ctrl-H'
        
        editMenu.Append(wx.ID_REPLACE, 'Replace Across Entire Story...\t' + shortcut)
        self.Bind(wx.EVT_MENU, self.showReplace, id = wx.ID_REPLACE)

        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_PREFERENCES, 'Preferences...\tCtrl-,')
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id = wx.ID_PREFERENCES)
        
        # View menu
 
        viewMenu = wx.Menu()
        
        viewMenu.Append(wx.ID_ZOOM_IN, 'Zoom &In\t=')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        viewMenu.Append(wx.ID_ZOOM_OUT, 'Zoom &Out\t-')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)
        
        viewMenu.Append(wx.ID_ZOOM_FIT, 'Zoom to &Fit\t0')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, 'Zoom &100%\t1')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom(1), id = wx.ID_ZOOM_100)
        
        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP, 'Snap to &Grid', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.toggleSnapping(), id = StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, '&Clean Up Passages')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cleanup(), id = StoryFrame.VIEW_CLEANUP)
        
        viewMenu.AppendSeparator()
        
        viewMenu.Append(StoryFrame.VIEW_TOOLBAR, '&Toolbar', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id = StoryFrame.VIEW_TOOLBAR)

        # Story menu

        self.storyMenu = wx.Menu()
        
        self.storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE, '&New Passage\tCtrl-N')
        self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id = StoryFrame.STORY_NEW_PASSAGE)
        
        self.storyMenu.Append(wx.ID_EDIT, '&Edit Passage\tCtrl-E')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e)), id = wx.ID_EDIT)

        self.storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN, '&Edit Passage Text Fullscreen\tF12')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen = True)), \
                  id = StoryFrame.STORY_EDIT_FULLSCREEN)
        
        self.storyMenu.Append(wx.ID_DELETE, '&Delete Passage')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)
 
        self.storyMenu.AppendSeparator()
        
        self.storyMenu.Append(StoryFrame.STORY_BUILD, '&Build Story...\tCtrl-B')
        self.Bind(wx.EVT_MENU, self.build, id = StoryFrame.STORY_BUILD)        
        
        self.storyMenu.Append(StoryFrame.STORY_REBUILD, '&Rebuild Story\tCtrl-R')
        self.Bind(wx.EVT_MENU, self.rebuild, id = StoryFrame.STORY_REBUILD) 

        self.storyMenu.Append(StoryFrame.STORY_VIEW_LAST, '&View Last Build\tCtrl-L')
        self.Bind(wx.EVT_MENU, self.viewBuild, id = StoryFrame.STORY_VIEW_LAST)
        
        self.autobuildmenuitem = self.storyMenu.Append(StoryFrame.STORY_AUTO_BUILD, '&Auto Build', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.autoBuild, self.autobuildmenuitem)
        self.storyMenu.Check(StoryFrame.STORY_AUTO_BUILD, False)

        self.storyMenu.AppendSeparator()

        self.storyMenu.Append(StoryFrame.STORY_STATS, 'Story &Statistics\tCtrl-I')
        self.Bind(wx.EVT_MENU, self.stats, id = StoryFrame.STORY_STATS) 

        # Story Format submenu
        
        storyFormatMenu = wx.Menu()
        storyFormatCounter = StoryFrame.STORY_FORMAT_BASE
        storyFormatPath = app.getPath() + os.sep + 'targets' + os.sep 
        for sfdir in os.listdir(storyFormatPath):
            if os.access(storyFormatPath + sfdir + os.sep + 'header.html', os.R_OK):
                if sfdir == 'jonah':
                    sfdirlabel = 'Jonah'
                elif sfdir == 'sugarcane': 
                    sfdirlabel = 'Sugarcane'
                else: 
                    sfdirlabel = sfdir.capitalize()
                storyFormatMenu.Append(storyFormatCounter, sfdirlabel, kind = wx.ITEM_CHECK)
                self.Bind(wx.EVT_MENU, lambda e,target=sfdir: self.setTarget(target), id = storyFormatCounter)
                self.storyFormats[storyFormatCounter] = sfdir
                storyFormatCounter = storyFormatCounter + 1
        
        storyFormatMenu.AppendSeparator()
       
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP, '&About Story Formats')        
        self.Bind(wx.EVT_MENU, lambda e: self.app.storyFormatHelp(), id = StoryFrame.STORY_FORMAT_HELP)
        
        self.storyMenu.AppendMenu(wx.ID_ANY, 'Story &Format', storyFormatMenu)
        
        # Help menu
        
        helpMenu = wx.Menu()
 
        helpMenu.Append(StoryFrame.HELP_MANUAL, 'Online &Help')
        self.Bind(wx.EVT_MENU, self.app.openDocs, id = StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, '&Discuss Twine Online')
        self.Bind(wx.EVT_MENU, self.app.openGroup, id = StoryFrame.HELP_GROUP)
        
        helpMenu.Append(StoryFrame.HELP_GITHUB, 'Twine on &GitHub')
        self.Bind(wx.EVT_MENU, self.app.openGitHub, id = StoryFrame.HELP_GITHUB)
        
        helpMenu.AppendSeparator()
        
        helpMenu.Append(wx.ID_ABOUT, '&About Twine')
        self.Bind(wx.EVT_MENU, self.app.about, id = wx.ID_ABOUT)
        
        # add menus
        
        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, '&File')
        self.menus.Append(editMenu, '&Edit')
        self.menus.Append(viewMenu, '&View')
        self.menus.Append(self.storyMenu, '&Story')
        self.menus.Append(helpMenu, '&Help')
        self.SetMenuBar(self.menus)
        
        # extra shortcuts
        
        self.SetAcceleratorTable(wx.AcceleratorTable([ \
                                    (wx.ACCEL_NORMAL, wx.WXK_RETURN, wx.ID_EDIT), \
                                    (wx.ACCEL_CTRL, wx.WXK_RETURN, StoryFrame.STORY_EDIT_FULLSCREEN) \
                                                      ]))

        # add toolbar
        if sys.platform == 'darwin':
            iconPath = re.sub('lib/.*', '', os.path.realpath(sys.path[0])) + "icons" + os.sep
        else:
            iconPath = self.app.getPath() + os.sep + 'icons' + os.sep
        
        self.toolbar = self.CreateToolBar(style = wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize((StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))
        
        self.toolbar.AddLabelTool(StoryFrame.STORY_NEW_PASSAGE, 'New Passage', \
                                  wx.Bitmap(iconPath + 'newpassage.png'), \
                                  shortHelp = StoryFrame.NEW_PASSAGE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.newWidget(), id = StoryFrame.STORY_NEW_PASSAGE)
        
        self.toolbar.AddSeparator()
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_IN, 'Zoom In', \
                                  wx.Bitmap(iconPath + 'zoomin.png'), \
                                  shortHelp = StoryFrame.ZOOM_IN_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_OUT, 'Zoom Out', \
                                  wx.Bitmap(iconPath + 'zoomout.png'), \
                                  shortHelp = StoryFrame.ZOOM_OUT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)  
          
        self.toolbar.AddLabelTool(wx.ID_ZOOM_FIT, 'Zoom to Fit', \
                                  wx.Bitmap(iconPath + 'zoomfit.png'), \
                                  shortHelp = StoryFrame.ZOOM_FIT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_100, 'Zoom to 100%', \
                                  wx.Bitmap(iconPath + 'zoom1.png'), \
                                  shortHelp = StoryFrame.ZOOM_ONE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom(1.0), id = wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)
        
        if app.config.ReadBool('storyFrameToolbar'):
            self.showToolbar = True
            self.toolbar.Realize()
        else:
            self.showToolbar = False
            self.toolbar.Realize()
            self.toolbar.Hide()
            
        
    def revert (self, event = None):
        """Reverts to the last saved version of the story file."""
        bits = os.path.splitext(self.saveDestination)
        title = '"' + os.path.basename(bits[0]) + '"'
        if title == '""': title = 'your story'
        
        message = 'Revert to the last saved version of ' + title + '?'
        dialog = wx.MessageDialog(self, message, 'Revert to Saved', wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)
        
        if (dialog.ShowModal() == wx.ID_YES):
            self.Destroy()
            self.app.open(self.saveDestination)
            self.dirty = False;
            self.checkClose(None)
    
    def checkClose (self, event):
        self.checkCloseDo(event,byMenu=False)
    
    def checkCloseMenu (self, event):
        self.checkCloseDo(event,byMenu=True)
        
    def checkCloseDo (self, event, byMenu):
        """
        If this instance's dirty flag is set, asks the user if they want to save the changes.
        """
                
        if (self.dirty):
            bits = os.path.splitext(self.saveDestination)
            title = '"' + os.path.basename(bits[0]) + '"'
            if title == '""': title = 'your story' 

            message = 'Do you want to save the changes to ' + title + ' before closing?'
            dialog = wx.MessageDialog(self, message, 'Unsaved Changes', \
                                      wx.ICON_WARNING | wx.YES_NO | wx.CANCEL | wx.YES_DEFAULT)
            result = dialog.ShowModal();
            if (result == wx.ID_CANCEL):
                event.Veto()
                return
            elif (result == wx.ID_NO):
                self.dirty = False
            else:
                self.save(None)
                if self.dirty:
                    event.Veto()
                    return
        
        # ask all our widgets to close any editor windows
        
        for w in list(self.storyPanel.widgets):
            if isinstance(w, PassageWidget):
                w.closeEditor()

        self.app.removeStory(self, byMenu)
        if event != None:
            event.Skip()
        self.Destroy()
        
    def saveAs (self, event = None):
        """Asks the user to choose a file to save state to, then passes off control to save()."""
        dialog = wx.FileDialog(self, 'Save Story As', os.getcwd(), "", \
                         "Twine Story (*.tws)|*.tws|Twine Story without private content [copy] (*.tws)|*.tws", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
    
        if dialog.ShowModal() == wx.ID_OK:
            if dialog.GetFilterIndex() == 0:
                self.saveDestination = dialog.GetPath()
                self.app.config.Write('savePath', os.getcwd())
                self.app.addRecentFile(self.saveDestination)
                self.save(None)
            elif dialog.GetFilterIndex() == 1:
                npsavedestination = dialog.GetPath()
                try:
                    dest = open(npsavedestination, 'wb')
                    pickle.dump(self.serialize_noprivate(npsavedestination), dest)
                    dest.close()
                    self.app.addRecentFile(npsavedestination)
                except:
                    self.app.displayError('saving your story')
                
        dialog.Destroy()
        
    def exportSource (self, event = None):
        """Asks the user to choose a file to export source to, then exports the wiki."""
        dialog = wx.FileDialog(self, 'Export Source Code', os.getcwd(), "", \
                               'Twee File (*.twee;* .tw; *.txt)|*.twee;*.tw;*.txt|All Files (*.*)|*.*', wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        if dialog.ShowModal() == wx.ID_OK:
            try:
                path = dialog.GetPath()
                tw = TiddlyWiki()
                
                for widget in self.storyPanel.widgets: tw.addTiddler(widget.passage)
                dest = codecs.open(path, 'w', 'utf-8-sig', 'replace')
                order = map(lambda w: w.passage.title, self.storyPanel.sortedWidgets())
                dest.write(tw.toTwee(order))
                dest.close()
            except:
                self.app.displayError('exporting your source code')

        dialog.Destroy()
        
    def importHtml (self, event = None):
        """Asks the user to choose a file to import HTML tiddlers from, then imports into the current story."""
        dialog = wx.FileDialog(self, 'Import From Compiled HTML', os.getcwd(), '', \
                               'HTML Twine game (*.html;* .htm; *.txt)|*.html;*.htm;*.txt|All Files (*.*)|*.*', wx.OPEN | wx.FD_CHANGE_DIR)
        
        if dialog.ShowModal() == wx.ID_OK:
            try:
                # have a TiddlyWiki object parse it for us
                tw = TiddlyWiki()
                tw.addHtmlFromFilename(dialog.GetPath())
                
                # add passages for each of the tiddlers the TiddlyWiki saw
                if len(tw.tiddlers):
                    lastpos = [0, 0]
                    for t in tw.tiddlers:
                        tiddler = tw.tiddlers[t]
                        new = self.storyPanel.newWidget(title = tiddler.title, text = tiddler.text, \
                                                        pos = tiddler.pos if tiddler.pos != None else lastpos, \
                                                        logicals = True, quietly = True)
                        lastpos = new.pos
                        new.passage.tags = tiddler.tags
                    self.setDirty(True, 'Import')
                else:
                    dialog = wx.MessageDialog(self, 'No passages were found in this file. Make sure ' + \
                                              'this is a Twine game file.', 'No Passages Found', \
                                              wx.ICON_INFORMATION | wx.OK)
                    dialog.ShowModal()
            except:
                self.app.displayError('importing from HTML') 
                   
    def importSource (self, event = None):
        """Asks the user to choose a file to import source from, then imports into the current story."""
        dialog = wx.FileDialog(self, 'Import Source Code', os.getcwd(), '', \
                               'Twee File (*.twee;* .tw; *.txt)|*.twee;*.tw;*.txt|All Files (*.*)|*.*', wx.OPEN | wx.FD_CHANGE_DIR)
        
        if dialog.ShowModal() == wx.ID_OK:
            try:
                # have a TiddlyWiki object parse it for us
                tw = TiddlyWiki()
                tw.addTweeFromFilename(dialog.GetPath())
                
                # add passages for each of the tiddlers the TiddlyWiki saw
                if len(tw.tiddlers):
                    lastpos = [0, 0]
                    for t in tw.tiddlers:
                        tiddler = tw.tiddlers[t]
                        new = self.storyPanel.newWidget(title = tiddler.title, text = tiddler.text, quietly = True, pos = lastpos)
                        new.passage.tags = tiddler.tags
                        lastpos = new.pos
                    self.setDirty(True, 'Import')
                else:
                    dialog = wx.MessageDialog(self, 'No passages were found in this file. Make sure ' + \
                                              'this is a Twee source file.', 'No Passages Found', \
                                              wx.ICON_INFORMATION | wx.OK)
                    dialog.ShowModal()
            except:
                self.app.displayError('importing your source code')
    
    def save (self, event = None):
        if (self.saveDestination == ''):
            self.saveAs()
            return
        
        try:
            dest = open(self.saveDestination, 'wb')
            pickle.dump(self.serialize(), dest)
            dest.close()
            self.setDirty(False)
            self.app.config.Write('LastFile', self.saveDestination)
        except:
            self.app.displayError('saving your story')

    def build (self, event = None):
        """Asks the user to choose a location to save a compiled story, then passed control to rebuild()."""
        dialog = wx.FileDialog(self, 'Build Story', os.getcwd(), "", \
                         "Web Page (*.html)|*.html", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
    
        if dialog.ShowModal() == wx.ID_OK:
            self.buildDestination = dialog.GetPath()
            self.rebuild(None, True)
        
        dialog.Destroy()
                
    def rebuild (self, event = None, displayAfter = False):
        """
        Builds an HTML version of the story. Pass whether to open the destination file afterwards.
        """        
        try:
            # Remember current working dir and set to savefile's dir. InterTwine StoryIncludes are relative to the Twine file.
            cwd = os.getcwd()
            if self.saveDestination == '':
                twinedocdir = cwd
            else:
                twinedocdir = os.path.dirname(self.saveDestination)
                os.chdir(twinedocdir)
    
            # assemble our tiddlywiki and write it out
            hasstartpassage = False
            tw = TiddlyWiki()
            for widget in self.storyPanel.widgets:
                if widget.passage.title != 'StoryIncludes' and \
                not any(t.startswith('Twine.') for t in widget.passage.tags):
                    widget.passage.pos = widget.pos
                    tw.addTiddler(widget.passage)
                    if widget.passage.title == "Start":
                        hasstartpassage = True

            # is there a Start passage?
            if hasstartpassage == False:
                self.app.displayError('building your story because there is no "Start" passage. ' + "\n" 
                                      + 'Your story will build but the web browser will not be able to run the story. ' + "\n"
                                      + 'Please add a passage with the title "Start"')

            for widget in self.storyPanel.widgets:
                if widget.passage.title == 'StoryIncludes':
                    lines = widget.passage.text.splitlines()
                    lines.append('');
                    # State 0: Look for a filename
                    ## State 1: have filename, look for filename, EXCEPT, INCLUDE, ALIAS
                    ## State 2: EXCEPT mode, look for INCLUDE 3, ALIAS 4 or blank line 0
                    ## State 3: INCLUDE mode, look for EXCEPT 2, ALIAS 4 or blank line 0
                    ## State 4: ALIAS mode, look for EXCEPT 2, INCLUDE 2 or blank line 0
                    state = 0;
                    state_filename = '';
                    excludepassages = TiddlyWiki.INFO_PASSAGES + ['Start']
                    for line in lines:
                        if state == 0:
                            state_filename = line
                            state = 1
                            continue
                        elif state == 1:
                            try:
                                if state_filename.strip() != '':
                                    extension = os.path.splitext(state_filename)[1] 
                                    if extension == '.tws':
                                        if any(state_filename.startswith(t) for t in ['http://', 'https://', 'ftp://']):
                                            openedFile = urllib.urlopen(state_filename)
                                        else:
                                            openedFile = open(state_filename, 'r')
                                        s = StoryFrame(None, app = self.app, state = pickle.load(openedFile))
                                        openedFile.close()
                                        for widget in s.storyPanel.widgets:
                                            if not any(widget.passage.title in t for t in excludepassages) and \
                                            not any('Twine.private' in t for t in widget.passage.tags) and \
                                            not any('Twine.system' in t for t in widget.passage.tags):
                                                tw.addTiddler(widget.passage)
                                        s.Destroy()
                                    elif extension == '.tw' or extension == '.txt' or extension == '.twee':
                                        if any(state_filename.startswith(t) for t in ['http://', 'https://', 'ftp://']):
                                            openedFile = urllib.urlopen(state_filename)
                                            s = openedFile.read()
                                            openedFile.close()
                                            t = tempfile.NamedTemporaryFile(delete=False)
                                            cleanuptempfile = True
                                            t.write(s)
                                            t.close()
                                            filename = t.name
                                        else:
                                            filename = state_filename
                                            cleanuptempfile = False
                                            
                                        tw1 = TiddlyWiki()
                                        tw1.addTweeFromFilename(filename)
                                        if cleanuptempfile: os.remove(filename)
                                        tiddlerkeys = tw1.tiddlers.keys()
                                        for tiddlerkey in tiddlerkeys:
                                            passage = tw1.tiddlers[tiddlerkey]
                                            if not any(passage.title == t for t in excludepassages) and \
                                            not any('Twine.private' in t for t in passage.tags) and \
                                            not any('Twine.system' in t for t in passage.tags):
                                                tw.addTiddler(passage)
                                    else:
                                        raise 'File format not recognized'
                            except:
                                self.app.displayError('opening the Twine file named ' + state_filename + ' which is referred to by the passage StoryIncludes')
                            state_filename = line
                            state = 1
                            continue
                    break
            
            # Decode story settings
            for widget in self.storyPanel.widgets:
                if widget.passage.title == 'StorySettings':
                    lines = widget.passage.text.splitlines()
                    for line in lines:
                        try:
                            (skey,svalue) = line.split(':')
                            tw.storysettings[skey.strip().lower()] = svalue.strip().lower()
                        except:
                            tw.storysettings[line.strip().lower()] = "true"
                    break
            
            # Write the output file
            os.chdir(os.path.dirname(self.buildDestination))
            dest = open(self.buildDestination, 'w')
            dest.write(tw.toHtml(self.app, self.target).encode('utf-8'))
            dest.close()
            os.chdir(cwd)
            if displayAfter: self.viewBuild()
        except:
            self.app.displayError('building your story')
    
    def viewBuild (self, event = None):
        """
        Opens the last built file in a Web browser.
        """
        path = 'file://' + urllib.pathname2url(self.buildDestination)
        path = path.replace('file://///', 'file:///')
        wx.LaunchDefaultBrowser(path)
        
    def autoBuild (self, event = None):
        """
        Toggles the autobuild feature
        """
        if self.autobuildmenuitem.IsChecked():
            self.autobuildtimer.Start(5000)
            self.autoBuildStart();
        else:
            self.autobuildtimer.Stop()
    
    def autoBuildTick (self, event = None):
        """
        Called whenever the autobuild timer checks up on things
        """
        for pathname, oldmtime in self.autobuildfiles.iteritems():
            newmtime = os.stat(pathname).st_mtime
            if newmtime != oldmtime:
                #print "Auto rebuild triggered by: ", pathname
                self.autobuildfiles[pathname] = newmtime
                self.rebuild()
                break
        
    def autoBuildStart (self):
        self.autobuildfiles = { }
        if self.saveDestination == '':
            twinedocdir = cwd
        else:
            twinedocdir = os.path.dirname(self.saveDestination)
        for f in os.listdir(twinedocdir):
            extension = os.path.splitext(f)[1] 
            if extension == '.tws' or extension == '.tw' or extension == '.txt' or extension == '.twee':
                pathname = os.path.join(twinedocdir, f)
                mtime = os.stat(pathname).st_mtime
                self.autobuildfiles[pathname] = mtime
        
    def stats (self, event = None):
        """
        Displays a StatisticsDialog for this frame.
        """
        
        statFrame = StatisticsDialog(parent = self, storyPanel = self.storyPanel, app = self.app)
        statFrame.ShowModal()

    def showFind (self, event = None):
        """
        Shows a StoryFindFrame for this frame.
        """

        if (not hasattr(self, 'findFrame')):
            self.findFrame = StoryFindFrame(self.storyPanel, self.app)
        else:
            try:
                self.findFrame.Raise()
            except wx._core.PyDeadObjectError:
                # user closed the frame, so we need to recreate it
                delattr(self, 'findFrame')
                self.showFind(event)

    def showReplace (self, event = None):
        """
        Shows a StoryReplaceFrame for this frame.
        """
        if (not hasattr(self, 'replaceFrame')):
            self.replaceFrame = StoryReplaceFrame(self.storyPanel, self.app)
        else:
            try:
                self.replaceFrame.Raise()
            except wx._core.PyDeadObjectError:
                # user closed the frame, so we need to recreate it
                delattr(self, 'replaceFrame')
                self.showReplace(event)

    def proof (self, event = None):
        """
        Builds an RTF version of the story. Pass whether to open the destination file afterwards.
        """
           
        # ask for our destination
        
        dialog = wx.FileDialog(self, 'Proof Story', os.getcwd(), "", \
                         "RTF Document (*.rtf)|*.rtf", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        
        if dialog.ShowModal() == wx.ID_OK:
            path = dialog.GetPath()
            dialog.Destroy()
        else:
            dialog.Destroy()
            return
        
        try:
            # open destination for writing
        
            dest = open(path, 'w')
            
            # assemble our tiddlywiki and write it out
            
            tw = TiddlyWiki()
            for widget in self.storyPanel.sortedWidgets():
                tw.addTiddler(widget.passage)

            order = map(lambda w: w.passage.title, self.storyPanel.sortedWidgets())            
            dest.write(tw.toRtf(order))
            dest.close()
        except:
            self.app.displayError('building a proofing copy of your story')
    
    def setTarget (self, target):
        self.target = target
        
    def updateUI (self, event = None):
        """Adjusts menu items to reflect the current state."""

        hasSelection = self.storyPanel.hasSelection()

        canPaste = False
        if wx.TheClipboard.Open():
            canPaste = wx.TheClipboard.IsSupported(wx.CustomDataFormat(StoryPanel.CLIPBOARD_FORMAT))
            wx.TheClipboard.Close()
        
        # window title
        
        if self.saveDestination == '':
            title = StoryFrame.DEFAULT_TITLE
        else:
            bits = os.path.splitext(self.saveDestination)
            title = os.path.basename(bits[0])
        
        percent = str(int(round(self.storyPanel.scale * 100)))
        dirty = ''
        if self.dirty: dirty = ' *'

        self.SetTitle(title + dirty + ' (' + percent + '%) ' + '- ' + self.app.NAME)
        
        # File menu
        
        revertItem = self.menus.FindItemById(wx.ID_REVERT_TO_SAVED)
        revertItem.Enable(self.saveDestination != '' and self.dirty)
        
        # Edit menu
        
        undoItem = self.menus.FindItemById(wx.ID_UNDO)
        undoItem.Enable(self.storyPanel.canUndo())
        if self.storyPanel.canUndo():
            undoItem.SetText('Undo ' + self.storyPanel.undoAction() + '\tCtrl-Z')
        else:
            undoItem.SetText("Can't Undo\tCtrl-Z")
        
        redoItem = self.menus.FindItemById(wx.ID_REDO)
        redoItem.Enable(self.storyPanel.canRedo())
        if self.storyPanel.canRedo():
            redoItem.SetText('Redo ' + self.storyPanel.redoAction() + '\tCtrl-Y')
        else:
            redoItem.SetText("Can't Redo\tCtrl-Y")
        
        cutItem = self.menus.FindItemById(wx.ID_CUT)
        cutItem.Enable(hasSelection)
        copyItem = self.menus.FindItemById(wx.ID_COPY)
        copyItem.Enable(hasSelection)
        deleteItem = self.menus.FindItemById(wx.ID_DELETE)
        deleteItem.Enable(hasSelection)      
        pasteItem = self.menus.FindItemById(wx.ID_PASTE)
        pasteItem.Enable(canPaste)
        
        findAgainItem = self.menus.FindItemById(StoryFrame.EDIT_FIND_NEXT)
        findAgainItem.Enable(self.storyPanel.lastSearchRegexp != None)
        
        # View menu
        
        toolbarItem = self.menus.FindItemById(StoryFrame.VIEW_TOOLBAR)
        toolbarItem.Check(self.showToolbar)
        snapItem = self.menus.FindItemById(StoryFrame.VIEW_SNAP)
        snapItem.Check(self.storyPanel.snapping)
        
        # Story menu
        
        editItem = self.menus.FindItemById(wx.ID_EDIT)
        editItem.Enable(hasSelection)
        
        editFullscreenItem = self.menus.FindItemById(StoryFrame.STORY_EDIT_FULLSCREEN)
        editFullscreenItem.Enable(hasSelection and not self.storyPanel.hasMultipleSelection())
        
        rebuildItem = self.menus.FindItemById(StoryFrame.STORY_REBUILD)
        rebuildItem.Enable(self.buildDestination != '')
        
        viewLastItem = self.menus.FindItemById(StoryFrame.STORY_VIEW_LAST)
        viewLastItem.Enable(self.buildDestination != '')
        
        # Story format submenu

        for key in self.storyFormats:
            self.menus.FindItemById(key).Check(self.target == self.storyFormats[key])
        
    def toggleToolbar (self, event = None):
        """Toggles the toolbar onscreen."""
        if (self.showToolbar):
            self.showToolbar = False
            self.toolbar.Hide()
            self.app.config.WriteBool('storyFrameToolbar', False)
        else:
            self.showToolbar = True
            self.toolbar.Show()
            self.app.config.WriteBool('storyFrameToolbar', True)
        self.SendSizeEvent()
        
    def setDirty (self, value, action = None):
        """
        Sets the dirty flag to the value passed. Make sure to use this instead of
        setting the dirty property directly, as this method automatically updates
        the pristine property as well.
        
        If you pass an action parameter, this action will be saved for undoing under
        that name.
        """
        self.dirty = value
        self.pristine = False
        
        if value is True and action:
            self.storyPanel.pushUndo(action)
    
    def applyPrefs (self):
        """Passes on the apply message to child widgets."""
        self.storyPanel.eachWidget(lambda w: w.applyPrefs())
        self.storyPanel.Refresh()
    
    def serialize (self):
        """Returns a dictionary of state suitable for pickling."""
        return { 'target': self.target, 'buildDestination': self.buildDestination, \
                 'saveDestination': self.saveDestination, \
                 'storyPanel': self.storyPanel.serialize() }
    
    def serialize_noprivate (self, dest):
        """Returns a dictionary of state suitable for pickling."""
        return { 'target': self.target, 'buildDestination': '', \
                 'saveDestination': dest, \
                 'storyPanel': self.storyPanel.serialize_noprivate() }

    def __repr__ (self):
        return "<StoryFrame '" + self.saveDestination + "'>"
    
    # menu constants
    # (that aren't already defined by wx)
    
    FILE_IMPORT_SOURCE = 101
    FILE_EXPORT_PROOF = 102
    FILE_EXPORT_SOURCE = 103
    FILE_IMPORT_HTML = 104
    
    EDIT_FIND_NEXT = 201
        
    VIEW_SNAP = 301
    VIEW_CLEANUP = 302
    VIEW_TOOLBAR = 303
    
    STORY_NEW_PASSAGE = 401
    STORY_EDIT_FULLSCREEN = 402
    STORY_BUILD = 403
    STORY_REBUILD = 404
    STORY_VIEW_LAST = 405
    STORY_AUTO_BUILD = 406
    STORY_STATS = 407
    
    STORY_FORMAT_HELP = 408
    STORY_FORMAT_BASE = 409    
    
    HELP_MANUAL = 501
    HELP_GROUP = 502
    HELP_GITHUB = 503

    # tooltip labels
    
    NEW_PASSAGE_TOOLTIP = 'Add a new passage to your story'
    ZOOM_IN_TOOLTIP = 'Zoom in'
    ZOOM_OUT_TOOLTIP = 'Zoom out'
    ZOOM_FIT_TOOLTIP = 'Zoom so all passages are visible onscreen'
    ZOOM_ONE_TOOLTIP = 'Zoom to 100%'

    # size constants
    
    DEFAULT_SIZE = (800, 600)
    TOOLBAR_ICON_SIZE = 32
    
    # misc stuff
    
    DEFAULT_TITLE = 'Untitled Story'
Example #4
0
class StoryFrame (wx.Frame):
    
    def __init__(self, parent, app, state = None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title = StoryFrame.DEFAULT_TITLE, \
                          size = StoryFrame.DEFAULT_SIZE)     
        self.app = app
        self.parent = parent
        self.pristine = True    # the user has not added any content to this at all
        self.dirty = False      # the user has not made unsaved changes

        # inner state
        
        if (state):
            self.buildDestination = state['buildDestination']
            self.saveDestination = state['saveDestination']
            self.target = state['target']
            self.storyPanel = StoryPanel(self, app, state = state['storyPanel'])
            self.pristine = False
        else:
            self.buildDestination = ''
            self.saveDestination = ''
            self.target = 'sugarcane'
            self.storyPanel = StoryPanel(self, app)
        
        # window events
        
        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)
        
        # File menu
        
        fileMenu = wx.Menu()
        
        fileMenu.Append(wx.ID_NEW, '&New Story\tCtrl-Shift-N')
        self.Bind(wx.EVT_MENU, self.app.newStory, id = wx.ID_NEW)
        
        fileMenu.Append(wx.ID_OPEN, '&Open Story...\tCtrl-O')
        self.Bind(wx.EVT_MENU, self.app.openDialog, id = wx.ID_OPEN)
        
        recentFilesMenu = wx.Menu()
        self.app.recentFiles.UseMenu(recentFilesMenu)
        self.app.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, 'Open &Recent', recentFilesMenu)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(0), id = wx.ID_FILE1)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(1), id = wx.ID_FILE2)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(2), id = wx.ID_FILE3)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(3), id = wx.ID_FILE4)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(4), id = wx.ID_FILE5)
        
        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_SAVE, '&Save Story\tCtrl-S')
        self.Bind(wx.EVT_MENU, self.save, id = wx.ID_SAVE)
        
        fileMenu.Append(wx.ID_SAVEAS, 'S&ave Story As...\tCtrl-Shift-S')
        self.Bind(wx.EVT_MENU, self.saveAs, id = wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved')
        self.Bind(wx.EVT_MENU, self.revert, id = wx.ID_REVERT_TO_SAVED)
        
        fileMenu.AppendSeparator()

        fileMenu.Append(StoryFrame.FILE_EXPORT_PROOF, 'Export &Proofing Copy...')
        self.Bind(wx.EVT_MENU, self.proof, id = StoryFrame.FILE_EXPORT_PROOF) 

        fileMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, '&Import Source Code...')
        self.Bind(wx.EVT_MENU, self.importSource, id = StoryFrame.FILE_IMPORT_SOURCE) 

        fileMenu.Append(StoryFrame.FILE_EXPORT_SOURCE, 'Export Source &Code...')
        self.Bind(wx.EVT_MENU, self.exportSource, id = StoryFrame.FILE_EXPORT_SOURCE)

        fileMenu.AppendSeparator()
        
        fileMenu.Append(wx.ID_CLOSE, '&Close Story\tCtrl-W')
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id = wx.ID_CLOSE)
        
        fileMenu.Append(wx.ID_EXIT, 'E&xit Twine\tCtrl-Q')
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id = wx.ID_EXIT)
        
        # Edit menu
        
        editMenu = wx.Menu()
        
        editMenu.Append(wx.ID_UNDO, '&Undo\tCtrl-Z')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id = wx.ID_UNDO)
        
        editMenu.Append(wx.ID_REDO, '&Redo\tCtrl-Y')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.redo(), id = wx.ID_REDO)

        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_CUT, 'Cu&t\tCtrl-X')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cutWidgets(), id = wx.ID_CUT)
        
        editMenu.Append(wx.ID_COPY, '&Copy\tCtrl-C')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.copyWidgets(), id = wx.ID_COPY)
        
        editMenu.Append(wx.ID_PASTE, '&Paste\tCtrl-V')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.pasteWidgets(), id = wx.ID_PASTE)
        
        editMenu.Append(wx.ID_DELETE, '&Delete\tDel')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, 'Select &All\tCtrl-A')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(True, exclusive = False)), id = wx.ID_SELECTALL)
        
        editMenu.AppendSeparator()
        
        editMenu.Append(wx.ID_PREFERENCES, 'Preferences...\tCtrl-,')
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id = wx.ID_PREFERENCES)
        
        # View menu
 
        viewMenu = wx.Menu()
        
        viewMenu.Append(wx.ID_ZOOM_IN, 'Zoom &In\t=')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        viewMenu.Append(wx.ID_ZOOM_OUT, 'Zoom &Out\t-')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)
        
        viewMenu.Append(wx.ID_ZOOM_FIT, 'Zoom to &Fit\t0')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, 'Zoom &100%\t1')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom(1), id = wx.ID_ZOOM_100)
        
        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP, 'Snap to &Grid', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.toggleSnapping(), id = StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, '&Clean Up Passages')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cleanup(), id = StoryFrame.VIEW_CLEANUP)
        
        viewMenu.AppendSeparator()
        
        viewMenu.Append(StoryFrame.VIEW_TOOLBAR, '&Toolbar', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id = StoryFrame.VIEW_TOOLBAR)

        # Story menu

        storyMenu = wx.Menu()
        
        storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE, '&New Passage\tCtrl-N')
        self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id = StoryFrame.STORY_NEW_PASSAGE)
        
        storyMenu.Append(wx.ID_EDIT, '&Edit Passage\tCtrl-E')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e)), id = wx.ID_EDIT)

        storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN, '&Edit Passage Text Fullscreen\tF12')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen = True)), \
                  id = StoryFrame.STORY_EDIT_FULLSCREEN)
        
        storyMenu.Append(wx.ID_DELETE, '&Delete Passage')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo = True), id = wx.ID_DELETE)
 
        storyMenu.AppendSeparator()
        
        storyMenu.Append(StoryFrame.STORY_BUILD, '&Build Story...\tCtrl-B')
        self.Bind(wx.EVT_MENU, self.build, id = StoryFrame.STORY_BUILD)        
        
        storyMenu.Append(StoryFrame.STORY_REBUILD, '&Rebuild Story\tCtrl-R')
        self.Bind(wx.EVT_MENU, self.rebuild, id = StoryFrame.STORY_REBUILD) 

        storyMenu.Append(StoryFrame.STORY_VIEW_LAST, '&View Last Build\tCtrl-L')
        self.Bind(wx.EVT_MENU, self.viewBuild, id = StoryFrame.STORY_VIEW_LAST)

        storyMenu.AppendSeparator()

        storyMenu.Append(StoryFrame.STORY_STATS, 'Story &Statistics\tCtrl-I')
        self.Bind(wx.EVT_MENU, self.stats, id = StoryFrame.STORY_STATS) 

        # Story Format submenu
        
        storyFormatMenu = wx.Menu()
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_SUGARCANE, '&Sugarcane', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('sugarcane'), id = StoryFrame.STORY_FORMAT_SUGARCANE) 
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_JONAH, '&Jonah', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('jonah'), id = StoryFrame.STORY_FORMAT_JONAH) 
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW2, 'TiddlyWiki &2', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('tw2'), id = StoryFrame.STORY_FORMAT_TW2) 
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW1, 'TiddlyWiki &1', kind = wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget('tw'), id = StoryFrame.STORY_FORMAT_TW1) 
        
        storyFormatMenu.AppendSeparator()
        
        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP, '&About Story Formats')        
        self.Bind(wx.EVT_MENU, lambda e: self.app.storyFormatHelp(), id = StoryFrame.STORY_FORMAT_HELP)
        
        storyMenu.AppendMenu(wx.ID_ANY, 'Story &Format', storyFormatMenu)
        
        # Help menu
        
        helpMenu = wx.Menu()
 
        helpMenu.Append(StoryFrame.HELP_MANUAL, 'Online &Help')
        self.Bind(wx.EVT_MENU, self.app.openDocs, id = StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, '&Discuss Twine Online')
        self.Bind(wx.EVT_MENU, self.app.openGroup, id = StoryFrame.HELP_GROUP)

        helpMenu.Append(StoryFrame.HELP_BUG, 'Report a &Bug')
        self.Bind(wx.EVT_MENU, self.app.reportBug, id = StoryFrame.HELP_BUG)
        
        helpMenu.AppendSeparator()
        
        helpMenu.Append(wx.ID_ABOUT, '&About Twine')
        self.Bind(wx.EVT_MENU, self.app.about, id = wx.ID_ABOUT)
        
        # add menus
        
        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, '&File')
        self.menus.Append(editMenu, '&Edit')
        self.menus.Append(viewMenu, '&View')
        self.menus.Append(storyMenu, '&Story')
        self.menus.Append(helpMenu, '&Help')
        self.SetMenuBar(self.menus)

        # add toolbar

        iconPath = self.app.getPath() + os.sep + 'icons' + os.sep
        
        self.toolbar = self.CreateToolBar(style = wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize((StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))
        
        self.toolbar.AddLabelTool(StoryFrame.STORY_NEW_PASSAGE, 'New Passage', \
                                  wx.Bitmap(iconPath + 'newpassage.png'), \
                                  shortHelp = StoryFrame.NEW_PASSAGE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.newWidget(), id = StoryFrame.STORY_NEW_PASSAGE)
        
        self.toolbar.AddSeparator()
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_IN, 'Zoom In', \
                                  wx.Bitmap(iconPath + 'zoomin.png'), \
                                  shortHelp = StoryFrame.ZOOM_IN_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('in'), id = wx.ID_ZOOM_IN)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_OUT, 'Zoom Out', \
                                  wx.Bitmap(iconPath + 'zoomout.png'), \
                                  shortHelp = StoryFrame.ZOOM_OUT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('out'), id = wx.ID_ZOOM_OUT)  
          
        self.toolbar.AddLabelTool(wx.ID_ZOOM_FIT, 'Zoom to Fit', \
                                  wx.Bitmap(iconPath + 'zoomfit.png'), \
                                  shortHelp = StoryFrame.ZOOM_FIT_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom('fit'), id = wx.ID_ZOOM_FIT)
        
        self.toolbar.AddLabelTool(wx.ID_ZOOM_100, 'Zoom to 100%', \
                                  wx.Bitmap(iconPath + 'zoom1.png'), \
                                  shortHelp = StoryFrame.ZOOM_ONE_TOOLTIP)
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom(1.0), id = wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)
        self.showToolbar = True
        self.toolbar.Realize()
        self.Show(True)
        
    def revert (self, event = None):
        """Reverts to the last saved version of the story file."""
        bits = os.path.splitext(self.saveDestination)
        title = os.path.basename(bits[0])
        message = 'Revert to the last version of ' + title + ' you saved?'
        dialog = wx.MessageDialog(self, message, 'Revert to Saved', wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)
        
        if (dialog.ShowModal() == wx.ID_YES):
            self.Destroy()
            self.open(self.saveDestination)
    
    def checkClose (self, event):
        """
        If this instance's dirty flag is set, asks the user to confirm that they don't want to save changes.
        """
                
        if (self.dirty):
            bits = os.path.splitext(self.saveDestination)
            title = os.path.basename(bits[0])

            message = 'Close ' + title + ' without saving changes?'
            dialog = wx.MessageDialog(self, message, 'Save Changes', \
                                      wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)
            if (dialog.ShowModal() == wx.ID_NO):
                event.Veto()
                return
        
        # ask all our widgets to close any editor windows
        
        map(lambda w: w.closeEditor(), self.storyPanel.widgets)
        self.app.removeStory(self)
        event.Skip()
        
    def saveAs (self, event = None):
        """Asks the user to choose a file to save state to, then passes off control to save()."""
        dialog = wx.FileDialog(self, 'Save Story As', os.getcwd(), "", \
                         "Twine Story (*.tws)|*.tws", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
    
        if dialog.ShowModal() == wx.ID_OK:
            self.saveDestination = dialog.GetPath()
            self.app.addRecentFile(self.saveDestination)
            self.save(None)
        
        dialog.Destroy()
        
    def exportSource (self, event = None):
        """Asks the user to choose a file to export source to, then exports the wiki."""
        dialog = wx.FileDialog(self, 'Export Source Code', os.getcwd(), "", \
                               "Text File (*.txt)|*.txt", wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        if dialog.ShowModal() == wx.ID_OK:
            try:
                path = dialog.GetPath()
                tw = TiddlyWiki()
                
                for widget in self.storyPanel.widgets: tw.addTiddler(widget.passage)
                dest = open(path, 'w')            
                dest.write(tw.toTwee())
                dest.close()
            except:
                self.app.displayError('exporting your source code')

        dialog.Destroy()
        
    def importSource (self, event = None):
        """Asks the user to choose a file to import source from, then imports into the current story."""
        dialog = wx.FileDialog(self, 'Import Source Code', os.getcwd(), '', \
                               'Text Files (*.txt)|*.txt|Twee Source Code (*.tw)|*.tw', wx.OPEN | wx.FD_CHANGE_DIR)
        
        if dialog.ShowModal() == wx.ID_OK:
            try:
                # have a TiddlyWiki object parse it for us
                
                source = open(dialog.GetPath(), 'r')
                tw = TiddlyWiki()
                tw.addTwee(source.read())
                source.close()
                
                # add passages for each of the tiddlers the TiddlyWiki saw
                
                if len(tw.tiddlers):
                    for t in tw.tiddlers:
                        tiddler = tw.tiddlers[t]
                        new = self.storyPanel.newWidget(title = tiddler.title, text = tiddler.text, quietly = True)
                        new.tags = tiddler.tags
                    self.setDirty(True, 'Import')
                else:
                    dialog = wx.MessageDialog(self, 'No passages were found in this file. Make sure ' + \
                                              'this is a Twee source file.', 'No Passages Found', \
                                              wx.ICON_INFO | wx.OK)
                    dialog.ShowModal()
            except:
                self.app.displayError('importing your source code')
    
    def save (self, event = None):
        if (self.saveDestination == ''):
            self.saveAs()
            return
        
        try:
            dest = open(self.saveDestination, 'w')
            pickle.dump(self.serialize(), dest)
            dest.close()
            self.setDirty(False)
        except:
            self.app.displayError('saving your story')

    def build (self, event = None):
        """Asks the user to choose a location to save a compiled story, then passed control to rebuild()."""
        dialog = wx.FileDialog(self, 'Build Story', os.getcwd(), "", \
                         "Web Page (*.html)|*.html", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
    
        if dialog.ShowModal() == wx.ID_OK:
            self.buildDestination = dialog.GetPath()
            self.rebuild(None, True)
        
        dialog.Destroy()
                
    def rebuild (self, event = None, displayAfter = False):
        """
        Builds an HTML version of the story. Pass whether to open the destination file afterwards.
        """
        try:
            # open destination for writing
            
            dest = open(self.buildDestination, 'w')
    
            # assemble our tiddlywiki and write it out
            
            tw = TiddlyWiki()
            
            for widget in self.storyPanel.widgets:
                tw.addTiddler(widget.passage)
            
            dest.write(tw.toHtml(self.app, self.target).encode('utf-8'))
            dest.close()        
            if displayAfter: self.viewBuild()
        except:
            self.app.displayError('building your story')
    
    def viewBuild (self, event = None):
        """
        Opens the last built file in a Web browser.
        """
        path = 'file://' + urllib.pathname2url(self.buildDestination)
        path = path.replace('file://///', 'file:///')
        wx.LaunchDefaultBrowser(path)        
        
    def stats (self, event = None):
        """
        Displays a StatisticsDialog for this frame.
        """
        
        statFrame = StatisticsDialog(parent = self, storyPanel = self.storyPanel, app = self.app)
        statFrame.ShowModal()

    def proof (self, event = None):
        """
        Builds an RTF version of the story. Pass whether to open the destination file afterwards.
        """
           
        # ask for our destination
        
        dialog = wx.FileDialog(self, 'Proof Story', os.getcwd(), "", \
                         "RTF Document (*.rtf)|*.rtf", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        
        if dialog.ShowModal() == wx.ID_OK:
            path = dialog.GetPath()
            dialog.Destroy()
        else:
            dialog.Destroy()
            return
        
        try:
            # open destination for writing
        
            dest = open(path, 'w')
            
            # assemble our tiddlywiki and write it out
            
            tw = TiddlyWiki()
            
            self.storyPanel.eachWidget(lambda w: tw.addTiddler(w.passage))
            dest.write(tw.toRtf())
            dest.close()
        except:
            self.app.displayError('building a proofing copy of your story')
        
    def setTarget (self, target):
        self.target = target
        
    def updateUI (self, event = None):
        """Adjusts menu items to reflect the current state."""

        hasSelection = self.storyPanel.hasSelection()

        canPaste = False
        if wx.TheClipboard.Open():
            canPaste = wx.TheClipboard.IsSupported(wx.CustomDataFormat(StoryPanel.CLIPBOARD_FORMAT))
            wx.TheClipboard.Close()
        
        # window title
        
        if self.saveDestination == '':
            title = StoryFrame.DEFAULT_TITLE
        else:
            bits = os.path.splitext(self.saveDestination)
            title = os.path.basename(bits[0])
        
        percent = str(int(round(self.storyPanel.scale * 100)))
        dirty = ''
        if self.dirty: dirty = ' *'

        self.SetTitle(title + dirty + ' (' + percent + '%) ' + '- ' + self.app.NAME)
        
        # File menu
        
        revertItem = self.menus.FindItemById(wx.ID_REVERT_TO_SAVED)
        revertItem.Enable(self.saveDestination != '' and self.dirty)
        
        # Edit menu
        
        undoItem = self.menus.FindItemById(wx.ID_UNDO)
        undoItem.Enable(self.storyPanel.canUndo())
        if self.storyPanel.canUndo():
            undoItem.SetText('Undo ' + self.storyPanel.undoAction() + '\tCtrl-Z')
        else:
            undoItem.SetText("Can't Undo\tCtrl-Z")
        
        redoItem = self.menus.FindItemById(wx.ID_REDO)
        redoItem.Enable(self.storyPanel.canRedo())
        if self.storyPanel.canRedo():
            redoItem.SetText('Redo ' + self.storyPanel.redoAction() + '\tCtrl-Y')
        else:
            redoItem.SetText("Can't Redo\tCtrl-Y")
        
        cutItem = self.menus.FindItemById(wx.ID_CUT)
        cutItem.Enable(hasSelection)
        copyItem = self.menus.FindItemById(wx.ID_COPY)
        copyItem.Enable(hasSelection)
        deleteItem = self.menus.FindItemById(wx.ID_DELETE)
        deleteItem.Enable(hasSelection)      
        pasteItem = self.menus.FindItemById(wx.ID_PASTE)
        pasteItem.Enable(canPaste)
        
        # View menu
        
        toolbarItem = self.menus.FindItemById(StoryFrame.VIEW_TOOLBAR)
        toolbarItem.Check(self.showToolbar)
        snapItem = self.menus.FindItemById(StoryFrame.VIEW_SNAP)
        snapItem.Check(self.storyPanel.snapping)
        
        # Story menu
        
        editItem = self.menus.FindItemById(wx.ID_EDIT)
        editItem.Enable(hasSelection)
        
        editFullscreenItem = self.menus.FindItemById(StoryFrame.STORY_EDIT_FULLSCREEN)
        editFullscreenItem.Enable(hasSelection and not self.storyPanel.hasMultipleSelection())
        
        rebuildItem = self.menus.FindItemById(StoryFrame.STORY_REBUILD)
        rebuildItem.Enable(self.buildDestination != '')
        
        viewLastItem = self.menus.FindItemById(StoryFrame.STORY_VIEW_LAST)
        viewLastItem.Enable(self.buildDestination != '')
        
        # Story format submenu
        
        formatItems = {}
        formatItems['sugarcane'] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_SUGARCANE)
        formatItems['jonah'] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_JONAH)
        formatItems['tw'] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_TW1)
        formatItems['tw2'] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_TW2)
        
        for key in formatItems:
            formatItems[key].Check(self.target == key)
        
    def toggleToolbar (self, event = None):
        """Toggles the toolbar onscreen."""
        if (self.showToolbar):
            self.showToolbar = False
            self.toolbar.Hide()
        else:
            self.showToolbar = True
            self.toolbar.Show()
        self.SendSizeEvent()
        
    def setDirty (self, value, action = None):
        """
        Sets the dirty flag to the value passed. Make sure to use this instead of
        setting the dirty property directly, as this method automatically updates
        the pristine property as well.
        
        If you pass an action parameter, this action will be saved for undoing under
        that name.
        """
        self.dirty = value
        self.pristine = False
        
        if value is True and action:
            self.storyPanel.pushUndo(action)
    
    def applyPrefs (self):
        """Passes on the apply message to child widgets."""
        self.storyPanel.eachWidget(lambda w: w.applyPrefs())
    
    def serialize (self):
        """Returns a dictionary of state suitable for pickling."""
        return { 'target': self.target, 'buildDestination': self.buildDestination, \
                 'saveDestination': self.saveDestination, \
                 'storyPanel': self.storyPanel.serialize() }
    
    def __repr__ (self):
        return "<StoryFrame '" + self.saveDestination + "'>"
    
    # menu constants
    # (that aren't already defined by wx)
    
    FILE_PAGE_SETUP = 101       # release 3 :)
    FILE_PRINT = 102            # release 3
    FILE_IMPORT_SOURCE = 103
    FILE_EXPORT_PROOF = 104
    FILE_EXPORT_SOURCE = 105
        
    VIEW_SNAP = 301
    VIEW_CLEANUP = 302
    VIEW_TOOLBAR = 303
    
    STORY_NEW_PASSAGE = 401
    STORY_EDIT_FULLSCREEN = 402
    STORY_BUILD = 403
    STORY_REBUILD = 404
    STORY_VIEW_LAST = 405
    STORY_STATS = 406
    
    STORY_FORMAT_SUGARCANE = 408
    STORY_FORMAT_JONAH = 409
    STORY_FORMAT_TW1 = 410
    STORY_FORMAT_TW2 = 411
    STORY_FORMAT_HELP = 412
    
    HELP_MANUAL = 501
    HELP_GROUP = 502
    HELP_BUG = 503

    # tooltip labels
    
    NEW_PASSAGE_TOOLTIP = 'Add a new passage to your story'
    ZOOM_IN_TOOLTIP = 'Zoom in'
    ZOOM_OUT_TOOLTIP = 'Zoom out'
    ZOOM_FIT_TOOLTIP = 'Zoom so all passages are visible onscreen'
    ZOOM_ONE_TOOLTIP = 'Zoom to 100%'

    # size constants
    
    DEFAULT_SIZE = (800, 600)
    TOOLBAR_ICON_SIZE = 32
    
    # misc stuff
    
    DEFAULT_TITLE = 'Untitled Story'
Example #5
0
    def __init__(self, parent, app, state=None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title=StoryFrame.DEFAULT_TITLE, size=StoryFrame.DEFAULT_SIZE)
        self.app = app
        self.parent = parent
        self.pristine = True  # the user has not added any content to this at all
        self.dirty = False  # the user has not made unsaved changes

        # inner state

        if state:
            self.buildDestination = state["buildDestination"]
            self.saveDestination = state["saveDestination"]
            self.target = state["target"]
            self.storyPanel = StoryPanel(self, app, state=state["storyPanel"])
            self.pristine = False
        else:
            self.buildDestination = ""
            self.saveDestination = ""
            self.target = "sugarcane"
            self.storyPanel = StoryPanel(self, app)

        # window events

        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)

        # File menu

        fileMenu = wx.Menu()

        fileMenu.Append(wx.ID_NEW, "&New Story\tCtrl-Shift-N")
        self.Bind(wx.EVT_MENU, self.app.newStory, id=wx.ID_NEW)

        fileMenu.Append(wx.ID_OPEN, "&Open Story...\tCtrl-O")
        self.Bind(wx.EVT_MENU, self.app.openDialog, id=wx.ID_OPEN)

        recentFilesMenu = wx.Menu()
        self.app.recentFiles.UseMenu(recentFilesMenu)
        self.app.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, "Open &Recent", recentFilesMenu)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(0), id=wx.ID_FILE1)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(1), id=wx.ID_FILE2)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(2), id=wx.ID_FILE3)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(3), id=wx.ID_FILE4)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(4), id=wx.ID_FILE5)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_SAVE, "&Save Story\tCtrl-S")
        self.Bind(wx.EVT_MENU, self.save, id=wx.ID_SAVE)

        fileMenu.Append(wx.ID_SAVEAS, "S&ave Story As...\tCtrl-Shift-S")
        self.Bind(wx.EVT_MENU, self.saveAs, id=wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, "&Revert to Saved")
        self.Bind(wx.EVT_MENU, self.revert, id=wx.ID_REVERT_TO_SAVED)

        fileMenu.AppendSeparator()

        fileMenu.Append(StoryFrame.FILE_EXPORT_PROOF, "Export &Proofing Copy...")
        self.Bind(wx.EVT_MENU, self.proof, id=StoryFrame.FILE_EXPORT_PROOF)

        fileMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, "&Import Source Code...")
        self.Bind(wx.EVT_MENU, self.importSource, id=StoryFrame.FILE_IMPORT_SOURCE)

        fileMenu.Append(StoryFrame.FILE_EXPORT_SOURCE, "Export Source &Code...")
        self.Bind(wx.EVT_MENU, self.exportSource, id=StoryFrame.FILE_EXPORT_SOURCE)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_CLOSE, "&Close Story\tCtrl-W")
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id=wx.ID_CLOSE)

        fileMenu.Append(wx.ID_EXIT, "E&xit Twine\tCtrl-Q")
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id=wx.ID_EXIT)

        # Edit menu

        editMenu = wx.Menu()

        editMenu.Append(wx.ID_UNDO, "&Undo\tCtrl-Z")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id=wx.ID_UNDO)

        editMenu.Append(wx.ID_REDO, "&Redo\tCtrl-Y")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.redo(), id=wx.ID_REDO)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_CUT, "Cu&t\tCtrl-X")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cutWidgets(), id=wx.ID_CUT)

        editMenu.Append(wx.ID_COPY, "&Copy\tCtrl-C")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.copyWidgets(), id=wx.ID_COPY)

        editMenu.Append(wx.ID_PASTE, "&Paste\tCtrl-V")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.pasteWidgets(), id=wx.ID_PASTE)

        editMenu.Append(wx.ID_DELETE, "&Delete\tDel")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo=True), id=wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, "Select &All\tCtrl-A")
        self.Bind(
            wx.EVT_MENU,
            lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(True, exclusive=False)),
            id=wx.ID_SELECTALL,
        )

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_FIND, "Find...\tCtrl-F")
        self.Bind(wx.EVT_MENU, self.showFind, id=wx.ID_FIND)

        editMenu.Append(StoryFrame.EDIT_FIND_NEXT, "Find Next\tCtrl-G")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.findWidgetRegexp(), id=StoryFrame.EDIT_FIND_NEXT)

        if sys.platform == "darwin":
            shortcut = "Ctrl-Shift-H"
        else:
            shortcut = "Ctrl-H"

        editMenu.Append(wx.ID_REPLACE, "Replace Across Entire Story...\t" + shortcut)
        self.Bind(wx.EVT_MENU, self.showReplace, id=wx.ID_REPLACE)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_PREFERENCES, "Preferences...\tCtrl-,")
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id=wx.ID_PREFERENCES)

        # View menu

        viewMenu = wx.Menu()

        viewMenu.Append(wx.ID_ZOOM_IN, "Zoom &In\t=")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom("in"), id=wx.ID_ZOOM_IN)

        viewMenu.Append(wx.ID_ZOOM_OUT, "Zoom &Out\t-")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom("out"), id=wx.ID_ZOOM_OUT)

        viewMenu.Append(wx.ID_ZOOM_FIT, "Zoom to &Fit\t0")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom("fit"), id=wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, "Zoom &100%\t1")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom(1), id=wx.ID_ZOOM_100)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP, "Snap to &Grid", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.toggleSnapping(), id=StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, "&Clean Up Passages")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cleanup(), id=StoryFrame.VIEW_CLEANUP)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_TOOLBAR, "&Toolbar", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id=StoryFrame.VIEW_TOOLBAR)

        # Story menu

        storyMenu = wx.Menu()

        storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE, "&New Passage\tCtrl-N")
        self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id=StoryFrame.STORY_NEW_PASSAGE)

        storyMenu.Append(wx.ID_EDIT, "&Edit Passage\tCtrl-E")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e)), id=wx.ID_EDIT)

        storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN, "&Edit Passage Text Fullscreen\tF12")
        self.Bind(
            wx.EVT_MENU,
            lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen=True)),
            id=StoryFrame.STORY_EDIT_FULLSCREEN,
        )

        storyMenu.Append(wx.ID_DELETE, "&Delete Passage")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo=True), id=wx.ID_DELETE)

        storyMenu.AppendSeparator()

        storyMenu.Append(StoryFrame.STORY_BUILD, "&Build Story...\tCtrl-B")
        self.Bind(wx.EVT_MENU, self.build, id=StoryFrame.STORY_BUILD)

        storyMenu.Append(StoryFrame.STORY_REBUILD, "&Rebuild Story\tCtrl-R")
        self.Bind(wx.EVT_MENU, self.rebuild, id=StoryFrame.STORY_REBUILD)

        storyMenu.Append(StoryFrame.STORY_VIEW_LAST, "&View Last Build\tCtrl-L")
        self.Bind(wx.EVT_MENU, self.viewBuild, id=StoryFrame.STORY_VIEW_LAST)

        storyMenu.AppendSeparator()

        storyMenu.Append(StoryFrame.STORY_STATS, "Story &Statistics\tCtrl-I")
        self.Bind(wx.EVT_MENU, self.stats, id=StoryFrame.STORY_STATS)

        # Story Format submenu

        storyFormatMenu = wx.Menu()

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_SUGARCANE, "&Sugarcane", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("sugarcane"), id=StoryFrame.STORY_FORMAT_SUGARCANE)

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_JONAH, "&Jonah", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("jonah"), id=StoryFrame.STORY_FORMAT_JONAH)

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW2, "TiddlyWiki &2", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("tw2"), id=StoryFrame.STORY_FORMAT_TW2)

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW1, "TiddlyWiki &1", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("tw"), id=StoryFrame.STORY_FORMAT_TW1)

        storyFormatMenu.AppendSeparator()

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP, "&About Story Formats")
        self.Bind(wx.EVT_MENU, lambda e: self.app.storyFormatHelp(), id=StoryFrame.STORY_FORMAT_HELP)

        storyMenu.AppendMenu(wx.ID_ANY, "Story &Format", storyFormatMenu)

        # Help menu

        helpMenu = wx.Menu()

        helpMenu.Append(StoryFrame.HELP_MANUAL, "Online &Help")
        self.Bind(wx.EVT_MENU, self.app.openDocs, id=StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, "&Discuss Twine Online")
        self.Bind(wx.EVT_MENU, self.app.openGroup, id=StoryFrame.HELP_GROUP)

        helpMenu.Append(StoryFrame.HELP_BUG, "Report a &Bug")
        self.Bind(wx.EVT_MENU, self.app.reportBug, id=StoryFrame.HELP_BUG)

        helpMenu.AppendSeparator()

        helpMenu.Append(wx.ID_ABOUT, "&About Twine")
        self.Bind(wx.EVT_MENU, self.app.about, id=wx.ID_ABOUT)

        # add menus

        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, "&File")
        self.menus.Append(editMenu, "&Edit")
        self.menus.Append(viewMenu, "&View")
        self.menus.Append(storyMenu, "&Story")
        self.menus.Append(helpMenu, "&Help")
        self.SetMenuBar(self.menus)

        # extra shortcuts

        self.SetAcceleratorTable(
            wx.AcceleratorTable(
                [
                    (wx.ACCEL_NORMAL, wx.WXK_RETURN, wx.ID_EDIT),
                    (wx.ACCEL_CTRL, wx.WXK_RETURN, StoryFrame.STORY_EDIT_FULLSCREEN),
                ]
            )
        )

        # add toolbar

        iconPath = self.app.getPath() + os.sep + "icons" + os.sep

        self.toolbar = self.CreateToolBar(style=wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize((StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))

        self.toolbar.AddLabelTool(
            StoryFrame.STORY_NEW_PASSAGE,
            "New Passage",
            wx.Bitmap(iconPath + "newpassage.png"),
            shortHelp=StoryFrame.NEW_PASSAGE_TOOLTIP,
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.newWidget(), id=StoryFrame.STORY_NEW_PASSAGE)

        self.toolbar.AddSeparator()

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_IN, "Zoom In", wx.Bitmap(iconPath + "zoomin.png"), shortHelp=StoryFrame.ZOOM_IN_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom("in"), id=wx.ID_ZOOM_IN)

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_OUT, "Zoom Out", wx.Bitmap(iconPath + "zoomout.png"), shortHelp=StoryFrame.ZOOM_OUT_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom("out"), id=wx.ID_ZOOM_OUT)

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_FIT, "Zoom to Fit", wx.Bitmap(iconPath + "zoomfit.png"), shortHelp=StoryFrame.ZOOM_FIT_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom("fit"), id=wx.ID_ZOOM_FIT)

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_100, "Zoom to 100%", wx.Bitmap(iconPath + "zoom1.png"), shortHelp=StoryFrame.ZOOM_ONE_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom(1.0), id=wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)

        if app.config.ReadBool("storyFrameToolbar"):
            self.showToolbar = True
            self.toolbar.Realize()
        else:
            self.showToolbar = False
            self.toolbar.Realize()
            self.toolbar.Hide()

        self.Show(True)
Example #6
0
class StoryFrame(wx.Frame):
    def __init__(self, parent, app, state=None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title=StoryFrame.DEFAULT_TITLE, size=StoryFrame.DEFAULT_SIZE)
        self.app = app
        self.parent = parent
        self.pristine = True  # the user has not added any content to this at all
        self.dirty = False  # the user has not made unsaved changes

        # inner state

        if state:
            self.buildDestination = state["buildDestination"]
            self.saveDestination = state["saveDestination"]
            self.target = state["target"]
            self.storyPanel = StoryPanel(self, app, state=state["storyPanel"])
            self.pristine = False
        else:
            self.buildDestination = ""
            self.saveDestination = ""
            self.target = "sugarcane"
            self.storyPanel = StoryPanel(self, app)

        # window events

        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)

        # File menu

        fileMenu = wx.Menu()

        fileMenu.Append(wx.ID_NEW, "&New Story\tCtrl-Shift-N")
        self.Bind(wx.EVT_MENU, self.app.newStory, id=wx.ID_NEW)

        fileMenu.Append(wx.ID_OPEN, "&Open Story...\tCtrl-O")
        self.Bind(wx.EVT_MENU, self.app.openDialog, id=wx.ID_OPEN)

        recentFilesMenu = wx.Menu()
        self.app.recentFiles.UseMenu(recentFilesMenu)
        self.app.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, "Open &Recent", recentFilesMenu)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(0), id=wx.ID_FILE1)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(1), id=wx.ID_FILE2)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(2), id=wx.ID_FILE3)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(3), id=wx.ID_FILE4)
        self.Bind(wx.EVT_MENU, lambda e: self.app.openRecent(4), id=wx.ID_FILE5)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_SAVE, "&Save Story\tCtrl-S")
        self.Bind(wx.EVT_MENU, self.save, id=wx.ID_SAVE)

        fileMenu.Append(wx.ID_SAVEAS, "S&ave Story As...\tCtrl-Shift-S")
        self.Bind(wx.EVT_MENU, self.saveAs, id=wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, "&Revert to Saved")
        self.Bind(wx.EVT_MENU, self.revert, id=wx.ID_REVERT_TO_SAVED)

        fileMenu.AppendSeparator()

        fileMenu.Append(StoryFrame.FILE_EXPORT_PROOF, "Export &Proofing Copy...")
        self.Bind(wx.EVT_MENU, self.proof, id=StoryFrame.FILE_EXPORT_PROOF)

        fileMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, "&Import Source Code...")
        self.Bind(wx.EVT_MENU, self.importSource, id=StoryFrame.FILE_IMPORT_SOURCE)

        fileMenu.Append(StoryFrame.FILE_EXPORT_SOURCE, "Export Source &Code...")
        self.Bind(wx.EVT_MENU, self.exportSource, id=StoryFrame.FILE_EXPORT_SOURCE)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_CLOSE, "&Close Story\tCtrl-W")
        self.Bind(wx.EVT_MENU, lambda e: self.Close(), id=wx.ID_CLOSE)

        fileMenu.Append(wx.ID_EXIT, "E&xit Twine\tCtrl-Q")
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id=wx.ID_EXIT)

        # Edit menu

        editMenu = wx.Menu()

        editMenu.Append(wx.ID_UNDO, "&Undo\tCtrl-Z")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id=wx.ID_UNDO)

        editMenu.Append(wx.ID_REDO, "&Redo\tCtrl-Y")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.redo(), id=wx.ID_REDO)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_CUT, "Cu&t\tCtrl-X")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cutWidgets(), id=wx.ID_CUT)

        editMenu.Append(wx.ID_COPY, "&Copy\tCtrl-C")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.copyWidgets(), id=wx.ID_COPY)

        editMenu.Append(wx.ID_PASTE, "&Paste\tCtrl-V")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.pasteWidgets(), id=wx.ID_PASTE)

        editMenu.Append(wx.ID_DELETE, "&Delete\tDel")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo=True), id=wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, "Select &All\tCtrl-A")
        self.Bind(
            wx.EVT_MENU,
            lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(True, exclusive=False)),
            id=wx.ID_SELECTALL,
        )

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_FIND, "Find...\tCtrl-F")
        self.Bind(wx.EVT_MENU, self.showFind, id=wx.ID_FIND)

        editMenu.Append(StoryFrame.EDIT_FIND_NEXT, "Find Next\tCtrl-G")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.findWidgetRegexp(), id=StoryFrame.EDIT_FIND_NEXT)

        if sys.platform == "darwin":
            shortcut = "Ctrl-Shift-H"
        else:
            shortcut = "Ctrl-H"

        editMenu.Append(wx.ID_REPLACE, "Replace Across Entire Story...\t" + shortcut)
        self.Bind(wx.EVT_MENU, self.showReplace, id=wx.ID_REPLACE)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_PREFERENCES, "Preferences...\tCtrl-,")
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id=wx.ID_PREFERENCES)

        # View menu

        viewMenu = wx.Menu()

        viewMenu.Append(wx.ID_ZOOM_IN, "Zoom &In\t=")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom("in"), id=wx.ID_ZOOM_IN)

        viewMenu.Append(wx.ID_ZOOM_OUT, "Zoom &Out\t-")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom("out"), id=wx.ID_ZOOM_OUT)

        viewMenu.Append(wx.ID_ZOOM_FIT, "Zoom to &Fit\t0")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom("fit"), id=wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, "Zoom &100%\t1")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.zoom(1), id=wx.ID_ZOOM_100)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP, "Snap to &Grid", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.toggleSnapping(), id=StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, "&Clean Up Passages")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.cleanup(), id=StoryFrame.VIEW_CLEANUP)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_TOOLBAR, "&Toolbar", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id=StoryFrame.VIEW_TOOLBAR)

        # Story menu

        storyMenu = wx.Menu()

        storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE, "&New Passage\tCtrl-N")
        self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id=StoryFrame.STORY_NEW_PASSAGE)

        storyMenu.Append(wx.ID_EDIT, "&Edit Passage\tCtrl-E")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e)), id=wx.ID_EDIT)

        storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN, "&Edit Passage Text Fullscreen\tF12")
        self.Bind(
            wx.EVT_MENU,
            lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen=True)),
            id=StoryFrame.STORY_EDIT_FULLSCREEN,
        )

        storyMenu.Append(wx.ID_DELETE, "&Delete Passage")
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.removeWidgets(e, saveUndo=True), id=wx.ID_DELETE)

        storyMenu.AppendSeparator()

        storyMenu.Append(StoryFrame.STORY_BUILD, "&Build Story...\tCtrl-B")
        self.Bind(wx.EVT_MENU, self.build, id=StoryFrame.STORY_BUILD)

        storyMenu.Append(StoryFrame.STORY_REBUILD, "&Rebuild Story\tCtrl-R")
        self.Bind(wx.EVT_MENU, self.rebuild, id=StoryFrame.STORY_REBUILD)

        storyMenu.Append(StoryFrame.STORY_VIEW_LAST, "&View Last Build\tCtrl-L")
        self.Bind(wx.EVT_MENU, self.viewBuild, id=StoryFrame.STORY_VIEW_LAST)

        storyMenu.AppendSeparator()

        storyMenu.Append(StoryFrame.STORY_STATS, "Story &Statistics\tCtrl-I")
        self.Bind(wx.EVT_MENU, self.stats, id=StoryFrame.STORY_STATS)

        # Story Format submenu

        storyFormatMenu = wx.Menu()

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_SUGARCANE, "&Sugarcane", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("sugarcane"), id=StoryFrame.STORY_FORMAT_SUGARCANE)

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_JONAH, "&Jonah", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("jonah"), id=StoryFrame.STORY_FORMAT_JONAH)

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW2, "TiddlyWiki &2", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("tw2"), id=StoryFrame.STORY_FORMAT_TW2)

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_TW1, "TiddlyWiki &1", kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, lambda e: self.setTarget("tw"), id=StoryFrame.STORY_FORMAT_TW1)

        storyFormatMenu.AppendSeparator()

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP, "&About Story Formats")
        self.Bind(wx.EVT_MENU, lambda e: self.app.storyFormatHelp(), id=StoryFrame.STORY_FORMAT_HELP)

        storyMenu.AppendMenu(wx.ID_ANY, "Story &Format", storyFormatMenu)

        # Help menu

        helpMenu = wx.Menu()

        helpMenu.Append(StoryFrame.HELP_MANUAL, "Online &Help")
        self.Bind(wx.EVT_MENU, self.app.openDocs, id=StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, "&Discuss Twine Online")
        self.Bind(wx.EVT_MENU, self.app.openGroup, id=StoryFrame.HELP_GROUP)

        helpMenu.Append(StoryFrame.HELP_BUG, "Report a &Bug")
        self.Bind(wx.EVT_MENU, self.app.reportBug, id=StoryFrame.HELP_BUG)

        helpMenu.AppendSeparator()

        helpMenu.Append(wx.ID_ABOUT, "&About Twine")
        self.Bind(wx.EVT_MENU, self.app.about, id=wx.ID_ABOUT)

        # add menus

        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, "&File")
        self.menus.Append(editMenu, "&Edit")
        self.menus.Append(viewMenu, "&View")
        self.menus.Append(storyMenu, "&Story")
        self.menus.Append(helpMenu, "&Help")
        self.SetMenuBar(self.menus)

        # extra shortcuts

        self.SetAcceleratorTable(
            wx.AcceleratorTable(
                [
                    (wx.ACCEL_NORMAL, wx.WXK_RETURN, wx.ID_EDIT),
                    (wx.ACCEL_CTRL, wx.WXK_RETURN, StoryFrame.STORY_EDIT_FULLSCREEN),
                ]
            )
        )

        # add toolbar

        iconPath = self.app.getPath() + os.sep + "icons" + os.sep

        self.toolbar = self.CreateToolBar(style=wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize((StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))

        self.toolbar.AddLabelTool(
            StoryFrame.STORY_NEW_PASSAGE,
            "New Passage",
            wx.Bitmap(iconPath + "newpassage.png"),
            shortHelp=StoryFrame.NEW_PASSAGE_TOOLTIP,
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.newWidget(), id=StoryFrame.STORY_NEW_PASSAGE)

        self.toolbar.AddSeparator()

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_IN, "Zoom In", wx.Bitmap(iconPath + "zoomin.png"), shortHelp=StoryFrame.ZOOM_IN_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom("in"), id=wx.ID_ZOOM_IN)

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_OUT, "Zoom Out", wx.Bitmap(iconPath + "zoomout.png"), shortHelp=StoryFrame.ZOOM_OUT_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom("out"), id=wx.ID_ZOOM_OUT)

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_FIT, "Zoom to Fit", wx.Bitmap(iconPath + "zoomfit.png"), shortHelp=StoryFrame.ZOOM_FIT_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom("fit"), id=wx.ID_ZOOM_FIT)

        self.toolbar.AddLabelTool(
            wx.ID_ZOOM_100, "Zoom to 100%", wx.Bitmap(iconPath + "zoom1.png"), shortHelp=StoryFrame.ZOOM_ONE_TOOLTIP
        )
        self.Bind(wx.EVT_TOOL, lambda e: self.storyPanel.zoom(1.0), id=wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)

        if app.config.ReadBool("storyFrameToolbar"):
            self.showToolbar = True
            self.toolbar.Realize()
        else:
            self.showToolbar = False
            self.toolbar.Realize()
            self.toolbar.Hide()

        self.Show(True)

    def revert(self, event=None):
        """Reverts to the last saved version of the story file."""
        bits = os.path.splitext(self.saveDestination)
        title = '"' + os.path.basename(bits[0]) + '"'
        if title == '""':
            title = "your story"

        message = "Revert to the last version of " + title + " you saved?"
        dialog = wx.MessageDialog(self, message, "Revert to Saved", wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)

        if dialog.ShowModal() == wx.ID_YES:
            self.Destroy()
            self.open(self.saveDestination)

    def checkClose(self, event):
        """
        If this instance's dirty flag is set, asks the user to confirm that they don't want to save changes.
        """

        if self.dirty:
            bits = os.path.splitext(self.saveDestination)
            title = '"' + os.path.basename(bits[0]) + '"'
            if title == '""':
                title = "your story"

            message = "Are you sure you want to close " + title + " without saving changes?"
            dialog = wx.MessageDialog(self, message, "Unsaved Changes", wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)
            if dialog.ShowModal() == wx.ID_NO:
                event.Veto()
                return

        # ask all our widgets to close any editor windows

        map(lambda w: w.closeEditor(), self.storyPanel.widgets)
        self.app.removeStory(self)
        event.Skip()

    def saveAs(self, event=None):
        """Asks the user to choose a file to save state to, then passes off control to save()."""
        dialog = wx.FileDialog(
            self,
            "Save Story As",
            os.getcwd(),
            "",
            "Twine Story (*.tws)|*.tws",
            wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR,
        )

        if dialog.ShowModal() == wx.ID_OK:
            self.saveDestination = dialog.GetPath()
            self.app.config.Write("savePath", os.getcwd())
            self.app.addRecentFile(self.saveDestination)
            self.save(None)

        dialog.Destroy()

    def exportSource(self, event=None):
        """Asks the user to choose a file to export source to, then exports the wiki."""
        dialog = wx.FileDialog(
            self,
            "Export Source Code",
            os.getcwd(),
            "",
            "Text File (*.txt)|*.txt",
            wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR,
        )
        if dialog.ShowModal() == wx.ID_OK:
            try:
                path = dialog.GetPath()
                tw = TiddlyWiki()

                for widget in self.storyPanel.widgets:
                    tw.addTiddler(widget.passage)
                dest = open(path, "w")
                order = map(lambda w: w.passage.title, self.storyPanel.sortedWidgets())
                dest.write(tw.toTwee(order))
                dest.close()
            except:
                self.app.displayError("exporting your source code")

        dialog.Destroy()

    def importSource(self, event=None):
        """Asks the user to choose a file to import source from, then imports into the current story."""
        dialog = wx.FileDialog(
            self,
            "Import Source Code",
            os.getcwd(),
            "",
            "Text Files (*.txt)|*.txt|Twee Source Code (*.tw)|*.tw",
            wx.OPEN | wx.FD_CHANGE_DIR,
        )

        if dialog.ShowModal() == wx.ID_OK:
            try:
                # have a TiddlyWiki object parse it for us

                source = open(dialog.GetPath(), "rb")
                tw = TiddlyWiki()
                tw.addTwee(source.read())
                source.close()

                # add passages for each of the tiddlers the TiddlyWiki saw

                if len(tw.tiddlers):
                    for t in tw.tiddlers:
                        tiddler = tw.tiddlers[t]
                        new = self.storyPanel.newWidget(title=tiddler.title, text=tiddler.text, quietly=True)
                        new.tags = tiddler.tags
                    self.setDirty(True, "Import")
                else:
                    dialog = wx.MessageDialog(
                        self,
                        "No passages were found in this file. Make sure " + "this is a Twee source file.",
                        "No Passages Found",
                        wx.ICON_INFO | wx.OK,
                    )
                    dialog.ShowModal()
            except:
                self.app.displayError("importing your source code")

    def save(self, event=None):
        if self.saveDestination == "":
            self.saveAs()
            return

        try:
            dest = open(self.saveDestination, "wb")
            pickle.dump(self.serialize(), dest)
            dest.close()
            self.setDirty(False)
        except:
            self.app.displayError("saving your story")

    def build(self, event=None):
        """Asks the user to choose a location to save a compiled story, then passed control to rebuild()."""
        dialog = wx.FileDialog(
            self,
            "Build Story",
            os.getcwd(),
            "",
            "Web Page (*.html)|*.html",
            wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR,
        )

        if dialog.ShowModal() == wx.ID_OK:
            self.buildDestination = dialog.GetPath()
            self.rebuild(None, True)

        dialog.Destroy()

    def rebuild(self, event=None, displayAfter=False):
        """
        Builds an HTML version of the story. Pass whether to open the destination file afterwards.
        """
        try:
            # open destination for writing

            dest = open(self.buildDestination, "w")

            # assemble our tiddlywiki and write it out

            tw = TiddlyWiki()
            for widget in self.storyPanel.widgets:
                tw.addTiddler(widget.passage)

            dest.write(tw.toHtml(self.app, self.target).encode("utf-8"))
            dest.close()
            if displayAfter:
                self.viewBuild()
        except:
            self.app.displayError("building your story")

    def viewBuild(self, event=None):
        """
        Opens the last built file in a Web browser.
        """
        path = "file://" + urllib.pathname2url(self.buildDestination)
        path = path.replace("file://///", "file:///")
        wx.LaunchDefaultBrowser(path)

    def stats(self, event=None):
        """
        Displays a StatisticsDialog for this frame.
        """

        statFrame = StatisticsDialog(parent=self, storyPanel=self.storyPanel, app=self.app)
        statFrame.ShowModal()

    def showFind(self, event=None):
        """
        Shows a StoryFindFrame for this frame.
        """

        if not hasattr(self, "findFrame"):
            self.findFrame = StoryFindFrame(self.storyPanel, self.app)
        else:
            try:
                self.findFrame.Raise()
            except wx._core.PyDeadObjectError:
                # user closed the frame, so we need to recreate it
                delattr(self, "findFrame")
                self.showFind(event)

    def showReplace(self, event=None):
        """
        Shows a StoryReplaceFrame for this frame.
        """
        if not hasattr(self, "replaceFrame"):
            self.replaceFrame = StoryReplaceFrame(self.storyPanel, self.app)
        else:
            try:
                self.replaceFrame.Raise()
            except wx._core.PyDeadObjectError:
                # user closed the frame, so we need to recreate it
                delattr(self, "replaceFrame")
                self.showReplace(event)

    def proof(self, event=None):
        """
        Builds an RTF version of the story. Pass whether to open the destination file afterwards.
        """

        # ask for our destination

        dialog = wx.FileDialog(
            self,
            "Proof Story",
            os.getcwd(),
            "",
            "RTF Document (*.rtf)|*.rtf",
            wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR,
        )

        if dialog.ShowModal() == wx.ID_OK:
            path = dialog.GetPath()
            dialog.Destroy()
        else:
            dialog.Destroy()
            return

        try:
            # open destination for writing

            dest = open(path, "w")

            # assemble our tiddlywiki and write it out

            tw = TiddlyWiki()
            for widget in self.storyPanel.sortedWidgets():
                tw.addTiddler(widget.passage)

            order = map(lambda w: w.passage.title, self.storyPanel.sortedWidgets())
            dest.write(tw.toRtf(order))
            dest.close()
        except:
            self.app.displayError("building a proofing copy of your story")

    def setTarget(self, target):
        self.target = target

    def updateUI(self, event=None):
        """Adjusts menu items to reflect the current state."""

        hasSelection = self.storyPanel.hasSelection()

        canPaste = False
        if wx.TheClipboard.Open():
            canPaste = wx.TheClipboard.IsSupported(wx.CustomDataFormat(StoryPanel.CLIPBOARD_FORMAT))
            wx.TheClipboard.Close()

        # window title

        if self.saveDestination == "":
            title = StoryFrame.DEFAULT_TITLE
        else:
            bits = os.path.splitext(self.saveDestination)
            title = os.path.basename(bits[0])

        percent = str(int(round(self.storyPanel.scale * 100)))
        dirty = ""
        if self.dirty:
            dirty = " *"

        self.SetTitle(title + dirty + " (" + percent + "%) " + "- " + self.app.NAME)

        # File menu

        revertItem = self.menus.FindItemById(wx.ID_REVERT_TO_SAVED)
        revertItem.Enable(self.saveDestination != "" and self.dirty)

        # Edit menu

        undoItem = self.menus.FindItemById(wx.ID_UNDO)
        undoItem.Enable(self.storyPanel.canUndo())
        if self.storyPanel.canUndo():
            undoItem.SetText("Undo " + self.storyPanel.undoAction() + "\tCtrl-Z")
        else:
            undoItem.SetText("Can't Undo\tCtrl-Z")

        redoItem = self.menus.FindItemById(wx.ID_REDO)
        redoItem.Enable(self.storyPanel.canRedo())
        if self.storyPanel.canRedo():
            redoItem.SetText("Redo " + self.storyPanel.redoAction() + "\tCtrl-Y")
        else:
            redoItem.SetText("Can't Redo\tCtrl-Y")

        cutItem = self.menus.FindItemById(wx.ID_CUT)
        cutItem.Enable(hasSelection)
        copyItem = self.menus.FindItemById(wx.ID_COPY)
        copyItem.Enable(hasSelection)
        deleteItem = self.menus.FindItemById(wx.ID_DELETE)
        deleteItem.Enable(hasSelection)
        pasteItem = self.menus.FindItemById(wx.ID_PASTE)
        pasteItem.Enable(canPaste)

        findAgainItem = self.menus.FindItemById(StoryFrame.EDIT_FIND_NEXT)
        findAgainItem.Enable(self.storyPanel.lastSearchRegexp != None)

        # View menu

        toolbarItem = self.menus.FindItemById(StoryFrame.VIEW_TOOLBAR)
        toolbarItem.Check(self.showToolbar)
        snapItem = self.menus.FindItemById(StoryFrame.VIEW_SNAP)
        snapItem.Check(self.storyPanel.snapping)

        # Story menu

        editItem = self.menus.FindItemById(wx.ID_EDIT)
        editItem.Enable(hasSelection)

        editFullscreenItem = self.menus.FindItemById(StoryFrame.STORY_EDIT_FULLSCREEN)
        editFullscreenItem.Enable(hasSelection and not self.storyPanel.hasMultipleSelection())

        rebuildItem = self.menus.FindItemById(StoryFrame.STORY_REBUILD)
        rebuildItem.Enable(self.buildDestination != "")

        viewLastItem = self.menus.FindItemById(StoryFrame.STORY_VIEW_LAST)
        viewLastItem.Enable(self.buildDestination != "")

        # Story format submenu

        formatItems = {}
        formatItems["sugarcane"] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_SUGARCANE)
        formatItems["jonah"] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_JONAH)
        formatItems["tw"] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_TW1)
        formatItems["tw2"] = self.menus.FindItemById(StoryFrame.STORY_FORMAT_TW2)

        for key in formatItems:
            formatItems[key].Check(self.target == key)

    def toggleToolbar(self, event=None):
        """Toggles the toolbar onscreen."""
        if self.showToolbar:
            self.showToolbar = False
            self.toolbar.Hide()
            self.app.config.WriteBool("storyFrameToolbar", False)
        else:
            self.showToolbar = True
            self.toolbar.Show()
            self.app.config.WriteBool("storyFrameToolbar", True)
        self.SendSizeEvent()

    def setDirty(self, value, action=None):
        """
        Sets the dirty flag to the value passed. Make sure to use this instead of
        setting the dirty property directly, as this method automatically updates
        the pristine property as well.
        
        If you pass an action parameter, this action will be saved for undoing under
        that name.
        """
        self.dirty = value
        self.pristine = False

        if value is True and action:
            self.storyPanel.pushUndo(action)

    def applyPrefs(self):
        """Passes on the apply message to child widgets."""
        self.storyPanel.eachWidget(lambda w: w.applyPrefs())
        self.storyPanel.Refresh()

    def serialize(self):
        """Returns a dictionary of state suitable for pickling."""
        return {
            "target": self.target,
            "buildDestination": self.buildDestination,
            "saveDestination": self.saveDestination,
            "storyPanel": self.storyPanel.serialize(),
        }

    def __repr__(self):
        return "<StoryFrame '" + self.saveDestination + "'>"

    # menu constants
    # (that aren't already defined by wx)

    FILE_PAGE_SETUP = 101  # release 3 :)
    FILE_PRINT = 102  # release 3
    FILE_IMPORT_SOURCE = 103
    FILE_EXPORT_PROOF = 104
    FILE_EXPORT_SOURCE = 105

    EDIT_FIND_NEXT = 201

    VIEW_SNAP = 301
    VIEW_CLEANUP = 302
    VIEW_TOOLBAR = 303

    STORY_NEW_PASSAGE = 401
    STORY_EDIT_FULLSCREEN = 402
    STORY_BUILD = 403
    STORY_REBUILD = 404
    STORY_VIEW_LAST = 405
    STORY_STATS = 406

    STORY_FORMAT_SUGARCANE = 408
    STORY_FORMAT_JONAH = 409
    STORY_FORMAT_TW1 = 410
    STORY_FORMAT_TW2 = 411
    STORY_FORMAT_HELP = 412

    HELP_MANUAL = 501
    HELP_GROUP = 502
    HELP_BUG = 503

    # tooltip labels

    NEW_PASSAGE_TOOLTIP = "Add a new passage to your story"
    ZOOM_IN_TOOLTIP = "Zoom in"
    ZOOM_OUT_TOOLTIP = "Zoom out"
    ZOOM_FIT_TOOLTIP = "Zoom so all passages are visible onscreen"
    ZOOM_ONE_TOOLTIP = "Zoom to 100%"

    # size constants

    DEFAULT_SIZE = (800, 600)
    TOOLBAR_ICON_SIZE = 32

    # misc stuff

    DEFAULT_TITLE = "Untitled Story"
Example #7
0
    def __init__(self, parent, app, state=None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title = StoryFrame.DEFAULT_TITLE, \
                          size = StoryFrame.DEFAULT_SIZE)
        self.app = app
        self.parent = parent
        self.pristine = True  # the user has not added any content to this at all
        self.dirty = False  # the user has not made unsaved changes
        self.storyFormats = {}  # list of available story formats

        # inner state

        if (state):
            self.buildDestination = state['buildDestination']
            self.saveDestination = state['saveDestination']
            self.target = state['target']
            self.storyPanel = StoryPanel(self, app, state=state['storyPanel'])
            self.pristine = False
        else:
            self.buildDestination = ''
            self.saveDestination = ''
            self.target = 'sugarcube'
            self.storyPanel = StoryPanel(self, app)

        # window events

        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)

        # Timer for the auto build file watcher
        self.autobuildtimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.autoBuildTick, self.autobuildtimer)

        # File menu

        fileMenu = wx.Menu()

        fileMenu.Append(wx.ID_NEW, '&New Story\tCtrl-Shift-N')
        self.Bind(wx.EVT_MENU, self.app.newStory, id=wx.ID_NEW)

        fileMenu.Append(wx.ID_OPEN, '&Open Story...\tCtrl-O')
        self.Bind(wx.EVT_MENU, self.app.openDialog, id=wx.ID_OPEN)

        recentFilesMenu = wx.Menu()
        self.recentFiles = wx.FileHistory(self.app.RECENT_FILES)
        self.recentFiles.Load(self.app.config)
        self.app.verifyRecentFiles(self)
        self.recentFiles.UseMenu(recentFilesMenu)
        self.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, 'Open &Recent', recentFilesMenu)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 0),
                  id=wx.ID_FILE1)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 1),
                  id=wx.ID_FILE2)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 2),
                  id=wx.ID_FILE3)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 3),
                  id=wx.ID_FILE4)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 4),
                  id=wx.ID_FILE5)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 5),
                  id=wx.ID_FILE6)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 6),
                  id=wx.ID_FILE7)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 7),
                  id=wx.ID_FILE8)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 8),
                  id=wx.ID_FILE9)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 9),
                  id=wx.ID_FILE9 + 1)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_SAVE, '&Save Story\tCtrl-S')
        self.Bind(wx.EVT_MENU, self.save, id=wx.ID_SAVE)

        fileMenu.Append(wx.ID_SAVEAS, 'S&ave Story As...\tCtrl-Shift-S')
        self.Bind(wx.EVT_MENU, self.saveAs, id=wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved')
        self.Bind(wx.EVT_MENU, self.revert, id=wx.ID_REVERT_TO_SAVED)

        fileMenu.AppendSeparator()

        fileMenu.Append(StoryFrame.FILE_EXPORT_PROOF,
                        'Export &Proofing Copy...')
        self.Bind(wx.EVT_MENU, self.proof, id=StoryFrame.FILE_EXPORT_PROOF)

        fileMenu.Append(StoryFrame.FILE_IMPORT_SOURCE,
                        '&Import Source Code...')
        self.Bind(wx.EVT_MENU,
                  self.importSource,
                  id=StoryFrame.FILE_IMPORT_SOURCE)

        fileMenu.Append(StoryFrame.FILE_EXPORT_SOURCE,
                        'Export Source &Code...')
        self.Bind(wx.EVT_MENU,
                  self.exportSource,
                  id=StoryFrame.FILE_EXPORT_SOURCE)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_CLOSE, '&Close Story\tCtrl-W')
        self.Bind(wx.EVT_MENU, self.checkCloseMenu, id=wx.ID_CLOSE)

        fileMenu.Append(wx.ID_EXIT, 'E&xit Twine\tCtrl-Q')
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id=wx.ID_EXIT)

        # Edit menu

        editMenu = wx.Menu()

        editMenu.Append(wx.ID_UNDO, '&Undo\tCtrl-Z')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id=wx.ID_UNDO)

        editMenu.Append(wx.ID_REDO, '&Redo\tCtrl-Y')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.redo(), id=wx.ID_REDO)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_CUT, 'Cu&t\tCtrl-X')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.cutWidgets(),
                  id=wx.ID_CUT)

        editMenu.Append(wx.ID_COPY, '&Copy\tCtrl-C')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.copyWidgets(),
                  id=wx.ID_COPY)

        editMenu.Append(wx.ID_PASTE, '&Paste\tCtrl-V')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.pasteWidgets(),
                  id=wx.ID_PASTE)

        editMenu.Append(wx.ID_DELETE, '&Delete\tDel')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.removeWidgets(e, saveUndo=True),
                  id=wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, 'Select &All\tCtrl-A')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(
                      True, exclusive=False)),
                  id=wx.ID_SELECTALL)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_FIND, 'Find...\tCtrl-F')
        self.Bind(wx.EVT_MENU, self.showFind, id=wx.ID_FIND)

        editMenu.Append(StoryFrame.EDIT_FIND_NEXT, 'Find Next\tCtrl-G')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.findWidgetRegexp(),
                  id=StoryFrame.EDIT_FIND_NEXT)

        if sys.platform == 'darwin':
            shortcut = 'Ctrl-Shift-H'
        else:
            shortcut = 'Ctrl-H'

        editMenu.Append(wx.ID_REPLACE,
                        'Replace Across Entire Story...\t' + shortcut)
        self.Bind(wx.EVT_MENU, self.showReplace, id=wx.ID_REPLACE)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_PREFERENCES, 'Preferences...\tCtrl-,')
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id=wx.ID_PREFERENCES)

        # View menu

        viewMenu = wx.Menu()

        viewMenu.Append(wx.ID_ZOOM_IN, 'Zoom &In\t=')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom('in'),
                  id=wx.ID_ZOOM_IN)

        viewMenu.Append(wx.ID_ZOOM_OUT, 'Zoom &Out\t-')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom('out'),
                  id=wx.ID_ZOOM_OUT)

        viewMenu.Append(wx.ID_ZOOM_FIT, 'Zoom to &Fit\t0')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom('fit'),
                  id=wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, 'Zoom &100%\t1')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom(1),
                  id=wx.ID_ZOOM_100)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP,
                        'Snap to &Grid',
                        kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.toggleSnapping(),
                  id=StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, '&Clean Up Passages')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.cleanup(),
                  id=StoryFrame.VIEW_CLEANUP)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_TOOLBAR,
                        '&Toolbar',
                        kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id=StoryFrame.VIEW_TOOLBAR)

        # Story menu

        self.storyMenu = wx.Menu()

        self.storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE,
                              '&New Passage\tCtrl-N')
        self.Bind(wx.EVT_MENU,
                  self.storyPanel.newWidget,
                  id=StoryFrame.STORY_NEW_PASSAGE)

        self.storyMenu.Append(wx.ID_EDIT, '&Edit Passage\tCtrl-E')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.
                                                               openEditor(e)),
                  id=wx.ID_EDIT)

        self.storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN,
                              '&Edit Passage Text Fullscreen\tF12')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen = True)), \
                  id = StoryFrame.STORY_EDIT_FULLSCREEN)

        self.storyMenu.Append(wx.ID_DELETE, '&Delete Passage')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.removeWidgets(e, saveUndo=True),
                  id=wx.ID_DELETE)

        self.storyMenu.AppendSeparator()

        self.storyMenu.Append(StoryFrame.STORY_BUILD,
                              '&Build Story...\tCtrl-B')
        self.Bind(wx.EVT_MENU, self.build, id=StoryFrame.STORY_BUILD)

        self.storyMenu.Append(StoryFrame.STORY_REBUILD,
                              '&Rebuild Story\tCtrl-R')
        self.Bind(wx.EVT_MENU, self.rebuild, id=StoryFrame.STORY_REBUILD)

        self.storyMenu.Append(StoryFrame.STORY_VIEW_LAST,
                              '&View Last Build\tCtrl-L')
        self.Bind(wx.EVT_MENU, self.viewBuild, id=StoryFrame.STORY_VIEW_LAST)

        self.autobuildmenuitem = self.storyMenu.Append(
            StoryFrame.STORY_AUTO_BUILD, '&Auto Build', kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.autoBuild, self.autobuildmenuitem)
        self.storyMenu.Check(StoryFrame.STORY_AUTO_BUILD, False)

        self.storyMenu.AppendSeparator()

        self.storyMenu.Append(StoryFrame.STORY_STATS,
                              'Story &Statistics\tCtrl-I')
        self.Bind(wx.EVT_MENU, self.stats, id=StoryFrame.STORY_STATS)

        # Story Format submenu

        storyFormatMenu = wx.Menu()
        storyFormatCounter = StoryFrame.STORY_FORMAT_BASE
        storyFormatPath = app.getPath() + os.sep + 'targets' + os.sep
        for sfdir in os.listdir(storyFormatPath):
            if os.access(storyFormatPath + sfdir + os.sep + 'header.html',
                         os.R_OK):
                if sfdir == 'jonah':
                    sfdirlabel = 'Jonah'
                elif sfdir == 'sugarcane':
                    sfdirlabel = 'Sugarcane'
                elif sfdir == 'sugarcube':
                    sfdirlabel = 'SugarCube'
                elif sfdir == 'tw':
                    sfdirlabel = 'TW'
                elif sfdir == 'tw2':
                    sfdirlabel = 'TW2'
                else:
                    sfdirlabel = sfdir
                storyFormatMenu.Append(storyFormatCounter,
                                       sfdirlabel,
                                       kind=wx.ITEM_CHECK)
                self.Bind(wx.EVT_MENU,
                          lambda e, target=sfdir: self.setTarget(target),
                          id=storyFormatCounter)
                self.storyFormats[storyFormatCounter] = sfdir
                storyFormatCounter = storyFormatCounter + 1

        storyFormatMenu.AppendSeparator()

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP,
                               '&About Story Formats')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.storyFormatHelp(),
                  id=StoryFrame.STORY_FORMAT_HELP)

        self.storyMenu.AppendMenu(wx.ID_ANY, 'Story &Format', storyFormatMenu)

        # Help menu

        helpMenu = wx.Menu()

        helpMenu.Append(StoryFrame.HELP_MANUAL, 'Online &Help')
        self.Bind(wx.EVT_MENU, self.app.openDocs, id=StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, '&Discuss Twine Online')
        self.Bind(wx.EVT_MENU, self.app.openGroup, id=StoryFrame.HELP_GROUP)

        helpMenu.Append(StoryFrame.HELP_BUG, 'Report a &Bug')
        self.Bind(wx.EVT_MENU, self.app.reportBug, id=StoryFrame.HELP_BUG)

        helpMenu.AppendSeparator()

        helpMenu.Append(wx.ID_ABOUT, '&About Twine')
        self.Bind(wx.EVT_MENU, self.app.about, id=wx.ID_ABOUT)

        # add menus

        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, '&File')
        self.menus.Append(editMenu, '&Edit')
        self.menus.Append(viewMenu, '&View')
        self.menus.Append(self.storyMenu, '&Story')
        self.menus.Append(helpMenu, '&Help')
        self.SetMenuBar(self.menus)

        # extra shortcuts

        self.SetAcceleratorTable(wx.AcceleratorTable([ \
                                    (wx.ACCEL_NORMAL, wx.WXK_RETURN, wx.ID_EDIT), \
                                    (wx.ACCEL_CTRL, wx.WXK_RETURN, StoryFrame.STORY_EDIT_FULLSCREEN) \
                                                      ]))

        # add toolbar

        iconPath = self.app.getPath() + os.sep + 'icons' + os.sep

        self.toolbar = self.CreateToolBar(style=wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize(
            (StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))

        self.toolbar.AddLabelTool(StoryFrame.STORY_NEW_PASSAGE, 'New Passage', \
                                  wx.Bitmap(iconPath + 'newpassage.png'), \
                                  shortHelp = StoryFrame.NEW_PASSAGE_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.newWidget(),
                  id=StoryFrame.STORY_NEW_PASSAGE)

        self.toolbar.AddSeparator()

        self.toolbar.AddLabelTool(wx.ID_ZOOM_IN, 'Zoom In', \
                                  wx.Bitmap(iconPath + 'zoomin.png'), \
                                  shortHelp = StoryFrame.ZOOM_IN_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom('in'),
                  id=wx.ID_ZOOM_IN)

        self.toolbar.AddLabelTool(wx.ID_ZOOM_OUT, 'Zoom Out', \
                                  wx.Bitmap(iconPath + 'zoomout.png'), \
                                  shortHelp = StoryFrame.ZOOM_OUT_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom('out'),
                  id=wx.ID_ZOOM_OUT)

        self.toolbar.AddLabelTool(wx.ID_ZOOM_FIT, 'Zoom to Fit', \
                                  wx.Bitmap(iconPath + 'zoomfit.png'), \
                                  shortHelp = StoryFrame.ZOOM_FIT_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom('fit'),
                  id=wx.ID_ZOOM_FIT)

        self.toolbar.AddLabelTool(wx.ID_ZOOM_100, 'Zoom to 100%', \
                                  wx.Bitmap(iconPath + 'zoom1.png'), \
                                  shortHelp = StoryFrame.ZOOM_ONE_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom(1.0),
                  id=wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)

        if app.config.ReadBool('storyFrameToolbar'):
            self.showToolbar = True
            self.toolbar.Realize()
        else:
            self.showToolbar = False
            self.toolbar.Realize()
            self.toolbar.Hide()
Example #8
0
class StoryFrame(wx.Frame):
    def __init__(self, parent, app, state=None):
        wx.Frame.__init__(self, parent, wx.ID_ANY, title = StoryFrame.DEFAULT_TITLE, \
                          size = StoryFrame.DEFAULT_SIZE)
        self.app = app
        self.parent = parent
        self.pristine = True  # the user has not added any content to this at all
        self.dirty = False  # the user has not made unsaved changes
        self.storyFormats = {}  # list of available story formats

        # inner state

        if (state):
            self.buildDestination = state['buildDestination']
            self.saveDestination = state['saveDestination']
            self.target = state['target']
            self.storyPanel = StoryPanel(self, app, state=state['storyPanel'])
            self.pristine = False
        else:
            self.buildDestination = ''
            self.saveDestination = ''
            self.target = 'sugarcube'
            self.storyPanel = StoryPanel(self, app)

        # window events

        self.Bind(wx.EVT_CLOSE, self.checkClose)
        self.Bind(wx.EVT_UPDATE_UI, self.updateUI)

        # Timer for the auto build file watcher
        self.autobuildtimer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.autoBuildTick, self.autobuildtimer)

        # File menu

        fileMenu = wx.Menu()

        fileMenu.Append(wx.ID_NEW, '&New Story\tCtrl-Shift-N')
        self.Bind(wx.EVT_MENU, self.app.newStory, id=wx.ID_NEW)

        fileMenu.Append(wx.ID_OPEN, '&Open Story...\tCtrl-O')
        self.Bind(wx.EVT_MENU, self.app.openDialog, id=wx.ID_OPEN)

        recentFilesMenu = wx.Menu()
        self.recentFiles = wx.FileHistory(self.app.RECENT_FILES)
        self.recentFiles.Load(self.app.config)
        self.app.verifyRecentFiles(self)
        self.recentFiles.UseMenu(recentFilesMenu)
        self.recentFiles.AddFilesToThisMenu(recentFilesMenu)
        fileMenu.AppendMenu(wx.ID_ANY, 'Open &Recent', recentFilesMenu)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 0),
                  id=wx.ID_FILE1)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 1),
                  id=wx.ID_FILE2)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 2),
                  id=wx.ID_FILE3)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 3),
                  id=wx.ID_FILE4)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 4),
                  id=wx.ID_FILE5)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 5),
                  id=wx.ID_FILE6)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 6),
                  id=wx.ID_FILE7)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 7),
                  id=wx.ID_FILE8)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 8),
                  id=wx.ID_FILE9)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.openRecent(self, 9),
                  id=wx.ID_FILE9 + 1)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_SAVE, '&Save Story\tCtrl-S')
        self.Bind(wx.EVT_MENU, self.save, id=wx.ID_SAVE)

        fileMenu.Append(wx.ID_SAVEAS, 'S&ave Story As...\tCtrl-Shift-S')
        self.Bind(wx.EVT_MENU, self.saveAs, id=wx.ID_SAVEAS)

        fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved')
        self.Bind(wx.EVT_MENU, self.revert, id=wx.ID_REVERT_TO_SAVED)

        fileMenu.AppendSeparator()

        fileMenu.Append(StoryFrame.FILE_EXPORT_PROOF,
                        'Export &Proofing Copy...')
        self.Bind(wx.EVT_MENU, self.proof, id=StoryFrame.FILE_EXPORT_PROOF)

        fileMenu.Append(StoryFrame.FILE_IMPORT_SOURCE,
                        '&Import Source Code...')
        self.Bind(wx.EVT_MENU,
                  self.importSource,
                  id=StoryFrame.FILE_IMPORT_SOURCE)

        fileMenu.Append(StoryFrame.FILE_EXPORT_SOURCE,
                        'Export Source &Code...')
        self.Bind(wx.EVT_MENU,
                  self.exportSource,
                  id=StoryFrame.FILE_EXPORT_SOURCE)

        fileMenu.AppendSeparator()

        fileMenu.Append(wx.ID_CLOSE, '&Close Story\tCtrl-W')
        self.Bind(wx.EVT_MENU, self.checkCloseMenu, id=wx.ID_CLOSE)

        fileMenu.Append(wx.ID_EXIT, 'E&xit Twine\tCtrl-Q')
        self.Bind(wx.EVT_MENU, lambda e: self.app.exit(), id=wx.ID_EXIT)

        # Edit menu

        editMenu = wx.Menu()

        editMenu.Append(wx.ID_UNDO, '&Undo\tCtrl-Z')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.undo(), id=wx.ID_UNDO)

        editMenu.Append(wx.ID_REDO, '&Redo\tCtrl-Y')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.redo(), id=wx.ID_REDO)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_CUT, 'Cu&t\tCtrl-X')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.cutWidgets(),
                  id=wx.ID_CUT)

        editMenu.Append(wx.ID_COPY, '&Copy\tCtrl-C')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.copyWidgets(),
                  id=wx.ID_COPY)

        editMenu.Append(wx.ID_PASTE, '&Paste\tCtrl-V')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.pasteWidgets(),
                  id=wx.ID_PASTE)

        editMenu.Append(wx.ID_DELETE, '&Delete\tDel')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.removeWidgets(e, saveUndo=True),
                  id=wx.ID_DELETE)

        editMenu.Append(wx.ID_SELECTALL, 'Select &All\tCtrl-A')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.eachWidget(lambda i: i.setSelected(
                      True, exclusive=False)),
                  id=wx.ID_SELECTALL)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_FIND, 'Find...\tCtrl-F')
        self.Bind(wx.EVT_MENU, self.showFind, id=wx.ID_FIND)

        editMenu.Append(StoryFrame.EDIT_FIND_NEXT, 'Find Next\tCtrl-G')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.findWidgetRegexp(),
                  id=StoryFrame.EDIT_FIND_NEXT)

        if sys.platform == 'darwin':
            shortcut = 'Ctrl-Shift-H'
        else:
            shortcut = 'Ctrl-H'

        editMenu.Append(wx.ID_REPLACE,
                        'Replace Across Entire Story...\t' + shortcut)
        self.Bind(wx.EVT_MENU, self.showReplace, id=wx.ID_REPLACE)

        editMenu.AppendSeparator()

        editMenu.Append(wx.ID_PREFERENCES, 'Preferences...\tCtrl-,')
        self.Bind(wx.EVT_MENU, self.app.showPrefs, id=wx.ID_PREFERENCES)

        # View menu

        viewMenu = wx.Menu()

        viewMenu.Append(wx.ID_ZOOM_IN, 'Zoom &In\t=')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom('in'),
                  id=wx.ID_ZOOM_IN)

        viewMenu.Append(wx.ID_ZOOM_OUT, 'Zoom &Out\t-')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom('out'),
                  id=wx.ID_ZOOM_OUT)

        viewMenu.Append(wx.ID_ZOOM_FIT, 'Zoom to &Fit\t0')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom('fit'),
                  id=wx.ID_ZOOM_FIT)

        viewMenu.Append(wx.ID_ZOOM_100, 'Zoom &100%\t1')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.zoom(1),
                  id=wx.ID_ZOOM_100)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_SNAP,
                        'Snap to &Grid',
                        kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.toggleSnapping(),
                  id=StoryFrame.VIEW_SNAP)

        viewMenu.Append(StoryFrame.VIEW_CLEANUP, '&Clean Up Passages')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.cleanup(),
                  id=StoryFrame.VIEW_CLEANUP)

        viewMenu.AppendSeparator()

        viewMenu.Append(StoryFrame.VIEW_TOOLBAR,
                        '&Toolbar',
                        kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.toggleToolbar, id=StoryFrame.VIEW_TOOLBAR)

        # Story menu

        self.storyMenu = wx.Menu()

        self.storyMenu.Append(StoryFrame.STORY_NEW_PASSAGE,
                              '&New Passage\tCtrl-N')
        self.Bind(wx.EVT_MENU,
                  self.storyPanel.newWidget,
                  id=StoryFrame.STORY_NEW_PASSAGE)

        self.storyMenu.Append(wx.ID_EDIT, '&Edit Passage\tCtrl-E')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.
                                                               openEditor(e)),
                  id=wx.ID_EDIT)

        self.storyMenu.Append(StoryFrame.STORY_EDIT_FULLSCREEN,
                              '&Edit Passage Text Fullscreen\tF12')
        self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: w.openEditor(e, fullscreen = True)), \
                  id = StoryFrame.STORY_EDIT_FULLSCREEN)

        self.storyMenu.Append(wx.ID_DELETE, '&Delete Passage')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.storyPanel.removeWidgets(e, saveUndo=True),
                  id=wx.ID_DELETE)

        self.storyMenu.AppendSeparator()

        self.storyMenu.Append(StoryFrame.STORY_BUILD,
                              '&Build Story...\tCtrl-B')
        self.Bind(wx.EVT_MENU, self.build, id=StoryFrame.STORY_BUILD)

        self.storyMenu.Append(StoryFrame.STORY_REBUILD,
                              '&Rebuild Story\tCtrl-R')
        self.Bind(wx.EVT_MENU, self.rebuild, id=StoryFrame.STORY_REBUILD)

        self.storyMenu.Append(StoryFrame.STORY_VIEW_LAST,
                              '&View Last Build\tCtrl-L')
        self.Bind(wx.EVT_MENU, self.viewBuild, id=StoryFrame.STORY_VIEW_LAST)

        self.autobuildmenuitem = self.storyMenu.Append(
            StoryFrame.STORY_AUTO_BUILD, '&Auto Build', kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU, self.autoBuild, self.autobuildmenuitem)
        self.storyMenu.Check(StoryFrame.STORY_AUTO_BUILD, False)

        self.storyMenu.AppendSeparator()

        self.storyMenu.Append(StoryFrame.STORY_STATS,
                              'Story &Statistics\tCtrl-I')
        self.Bind(wx.EVT_MENU, self.stats, id=StoryFrame.STORY_STATS)

        # Story Format submenu

        storyFormatMenu = wx.Menu()
        storyFormatCounter = StoryFrame.STORY_FORMAT_BASE
        storyFormatPath = app.getPath() + os.sep + 'targets' + os.sep
        for sfdir in os.listdir(storyFormatPath):
            if os.access(storyFormatPath + sfdir + os.sep + 'header.html',
                         os.R_OK):
                if sfdir == 'jonah':
                    sfdirlabel = 'Jonah'
                elif sfdir == 'sugarcane':
                    sfdirlabel = 'Sugarcane'
                elif sfdir == 'sugarcube':
                    sfdirlabel = 'SugarCube'
                elif sfdir == 'tw':
                    sfdirlabel = 'TW'
                elif sfdir == 'tw2':
                    sfdirlabel = 'TW2'
                else:
                    sfdirlabel = sfdir
                storyFormatMenu.Append(storyFormatCounter,
                                       sfdirlabel,
                                       kind=wx.ITEM_CHECK)
                self.Bind(wx.EVT_MENU,
                          lambda e, target=sfdir: self.setTarget(target),
                          id=storyFormatCounter)
                self.storyFormats[storyFormatCounter] = sfdir
                storyFormatCounter = storyFormatCounter + 1

        storyFormatMenu.AppendSeparator()

        storyFormatMenu.Append(StoryFrame.STORY_FORMAT_HELP,
                               '&About Story Formats')
        self.Bind(wx.EVT_MENU,
                  lambda e: self.app.storyFormatHelp(),
                  id=StoryFrame.STORY_FORMAT_HELP)

        self.storyMenu.AppendMenu(wx.ID_ANY, 'Story &Format', storyFormatMenu)

        # Help menu

        helpMenu = wx.Menu()

        helpMenu.Append(StoryFrame.HELP_MANUAL, 'Online &Help')
        self.Bind(wx.EVT_MENU, self.app.openDocs, id=StoryFrame.HELP_MANUAL)

        helpMenu.Append(StoryFrame.HELP_GROUP, '&Discuss Twine Online')
        self.Bind(wx.EVT_MENU, self.app.openGroup, id=StoryFrame.HELP_GROUP)

        helpMenu.Append(StoryFrame.HELP_BUG, 'Report a &Bug')
        self.Bind(wx.EVT_MENU, self.app.reportBug, id=StoryFrame.HELP_BUG)

        helpMenu.AppendSeparator()

        helpMenu.Append(wx.ID_ABOUT, '&About Twine')
        self.Bind(wx.EVT_MENU, self.app.about, id=wx.ID_ABOUT)

        # add menus

        self.menus = wx.MenuBar()
        self.menus.Append(fileMenu, '&File')
        self.menus.Append(editMenu, '&Edit')
        self.menus.Append(viewMenu, '&View')
        self.menus.Append(self.storyMenu, '&Story')
        self.menus.Append(helpMenu, '&Help')
        self.SetMenuBar(self.menus)

        # extra shortcuts

        self.SetAcceleratorTable(wx.AcceleratorTable([ \
                                    (wx.ACCEL_NORMAL, wx.WXK_RETURN, wx.ID_EDIT), \
                                    (wx.ACCEL_CTRL, wx.WXK_RETURN, StoryFrame.STORY_EDIT_FULLSCREEN) \
                                                      ]))

        # add toolbar

        iconPath = self.app.getPath() + os.sep + 'icons' + os.sep

        self.toolbar = self.CreateToolBar(style=wx.TB_FLAT | wx.TB_NODIVIDER)
        self.toolbar.SetToolBitmapSize(
            (StoryFrame.TOOLBAR_ICON_SIZE, StoryFrame.TOOLBAR_ICON_SIZE))

        self.toolbar.AddLabelTool(StoryFrame.STORY_NEW_PASSAGE, 'New Passage', \
                                  wx.Bitmap(iconPath + 'newpassage.png'), \
                                  shortHelp = StoryFrame.NEW_PASSAGE_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.newWidget(),
                  id=StoryFrame.STORY_NEW_PASSAGE)

        self.toolbar.AddSeparator()

        self.toolbar.AddLabelTool(wx.ID_ZOOM_IN, 'Zoom In', \
                                  wx.Bitmap(iconPath + 'zoomin.png'), \
                                  shortHelp = StoryFrame.ZOOM_IN_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom('in'),
                  id=wx.ID_ZOOM_IN)

        self.toolbar.AddLabelTool(wx.ID_ZOOM_OUT, 'Zoom Out', \
                                  wx.Bitmap(iconPath + 'zoomout.png'), \
                                  shortHelp = StoryFrame.ZOOM_OUT_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom('out'),
                  id=wx.ID_ZOOM_OUT)

        self.toolbar.AddLabelTool(wx.ID_ZOOM_FIT, 'Zoom to Fit', \
                                  wx.Bitmap(iconPath + 'zoomfit.png'), \
                                  shortHelp = StoryFrame.ZOOM_FIT_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom('fit'),
                  id=wx.ID_ZOOM_FIT)

        self.toolbar.AddLabelTool(wx.ID_ZOOM_100, 'Zoom to 100%', \
                                  wx.Bitmap(iconPath + 'zoom1.png'), \
                                  shortHelp = StoryFrame.ZOOM_ONE_TOOLTIP)
        self.Bind(wx.EVT_TOOL,
                  lambda e: self.storyPanel.zoom(1.0),
                  id=wx.ID_ZOOM_100)

        self.SetIcon(self.app.icon)

        if app.config.ReadBool('storyFrameToolbar'):
            self.showToolbar = True
            self.toolbar.Realize()
        else:
            self.showToolbar = False
            self.toolbar.Realize()
            self.toolbar.Hide()

    def revert(self, event=None):
        """Reverts to the last saved version of the story file."""
        bits = os.path.splitext(self.saveDestination)
        title = '"' + os.path.basename(bits[0]) + '"'
        if title == '""': title = 'your story'

        message = 'Revert to the last version of ' + title + ' you saved?'
        dialog = wx.MessageDialog(self, message, 'Revert to Saved',
                                  wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)

        if (dialog.ShowModal() == wx.ID_YES):
            self.Destroy()
            self.app.open(self.saveDestination)
            self.dirty = False
            self.checkClose(None)

    def checkClose(self, event):
        self.checkCloseDo(event, byMenu=False)

    def checkCloseMenu(self, event):
        self.checkCloseDo(event, byMenu=True)

    def checkCloseDo(self, event, byMenu):
        """
        If this instance's dirty flag is set, asks the user to confirm that they don't want to save changes.
        """

        if (self.dirty):
            bits = os.path.splitext(self.saveDestination)
            title = '"' + os.path.basename(bits[0]) + '"'
            if title == '""': title = 'your story'

            message = 'Are you sure you want to close ' + title + ' without saving changes?'
            dialog = wx.MessageDialog(self, message, 'Unsaved Changes', \
                                      wx.ICON_WARNING | wx.YES_NO | wx.NO_DEFAULT)
            if (dialog.ShowModal() == wx.ID_NO):
                event.Veto()
                return
            else:
                self.dirty = False

        # ask all our widgets to close any editor windows

        for w in list(self.storyPanel.widgets):
            if isinstance(w, PassageWidget):
                w.closeEditor()

        self.app.removeStory(self, byMenu)
        if event != None:
            event.Skip()
        self.Destroy()

    def saveAs(self, event=None):
        """Asks the user to choose a file to save state to, then passes off control to save()."""
        dialog = wx.FileDialog(self, 'Save Story As', os.getcwd(), "", \
                         "Twine Story (*.tws)|*.tws|Twine Story without private content [copy] (*.tws)|*.tws", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)

        if dialog.ShowModal() == wx.ID_OK:
            if dialog.GetFilterIndex() == 0:
                self.saveDestination = dialog.GetPath()
                self.app.config.Write('savePath', os.getcwd())
                self.app.addRecentFile(self.saveDestination)
                self.save(None)
            elif dialog.GetFilterIndex() == 1:
                npsavedestination = dialog.GetPath()
                try:
                    dest = open(npsavedestination, 'wb')
                    pickle.dump(self.serialize_noprivate(npsavedestination),
                                dest)
                    dest.close()
                    self.app.addRecentFile(npsavedestination)
                except:
                    self.app.displayError('saving your story')

        dialog.Destroy()

    def exportSource(self, event=None):
        """Asks the user to choose a file to export source to, then exports the wiki."""
        dialog = wx.FileDialog(self, 'Export Source Code', os.getcwd(), "", \
                               'Twee File (*.twee;* .tw; *.txt)|*.twee;*.tw;*.txt|All Files (*.*)|*.*', wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)
        if dialog.ShowModal() == wx.ID_OK:
            try:
                path = dialog.GetPath()
                tw = TiddlyWiki()

                for widget in self.storyPanel.widgets:
                    tw.addTiddler(widget.passage)
                dest = codecs.open(path, 'w', 'utf-8-sig', 'replace')
                order = map(lambda w: w.passage.title,
                            self.storyPanel.sortedWidgets())
                dest.write(tw.toTwee(order))
                dest.close()
            except:
                self.app.displayError('exporting your source code')

        dialog.Destroy()

    def importSource(self, event=None):
        """Asks the user to choose a file to import source from, then imports into the current story."""
        dialog = wx.FileDialog(self, 'Import Source Code', os.getcwd(), '', \
                               'Twee File (*.twee;* .tw; *.txt)|*.twee;*.tw;*.txt|All Files (*.*)|*.*', wx.OPEN | wx.FD_CHANGE_DIR)

        if dialog.ShowModal() == wx.ID_OK:
            try:
                # have a TiddlyWiki object parse it for us
                tw = TiddlyWiki()
                tw.addTweeFromFilename(dialog.GetPath())

                # add passages for each of the tiddlers the TiddlyWiki saw

                if len(tw.tiddlers):
                    for t in tw.tiddlers:
                        tiddler = tw.tiddlers[t]
                        new = self.storyPanel.newWidget(title=tiddler.title,
                                                        text=tiddler.text,
                                                        quietly=True)
                        new.tags = tiddler.tags
                    self.setDirty(True, 'Import')
                else:
                    dialog = wx.MessageDialog(self, 'No passages were found in this file. Make sure ' + \
                                              'this is a Twee source file.', 'No Passages Found', \
                                              wx.ICON_INFO | wx.OK)
                    dialog.ShowModal()
            except:
                self.app.displayError('importing your source code')

    def save(self, event=None):
        if (self.saveDestination == ''):
            self.saveAs()
            return

        try:
            dest = open(self.saveDestination, 'wb')
            pickle.dump(self.serialize(), dest)
            dest.close()
            self.setDirty(False)
            self.app.config.Write('LastFile', self.saveDestination)
        except:
            self.app.displayError('saving your story')

    def build(self, event=None):
        """Asks the user to choose a location to save a compiled story, then passed control to rebuild()."""
        dialog = wx.FileDialog(self, 'Build Story', os.getcwd(), "", \
                         "Web Page (*.html)|*.html", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)

        if dialog.ShowModal() == wx.ID_OK:
            self.buildDestination = dialog.GetPath()
            self.rebuild(None, True)

        dialog.Destroy()

    def rebuild(self, event=None, displayAfter=False):
        """
        Builds an HTML version of the story. Pass whether to open the destination file afterwards.
        """
        try:
            # Remember current working dir and set to savefile's dir. InterTwine StoryIncludes are relative to the Twine file.
            cwd = os.getcwd()
            if self.saveDestination == '':
                twinedocdir = cwd
            else:
                twinedocdir = os.path.dirname(self.saveDestination)
                os.chdir(twinedocdir)
            if not self.storyPanel.findWidget('StoryIncludePassages'):
                self.storyPanel.newWidget(title='StoryIncludePassages',
                                          pos=(-1000, -1000),
                                          quietly=True)
                self.storyPanel.findWidget(
                    'StoryIncludePassages').passage.tags.append('Twine.hide')
                self.storyPanel.findWidget(
                    'StoryIncludePassages').passage.tags.append('Twine.system')
                self.storyPanel.findWidget('StoryIncludePassages').pos = [
                    -1000, -1000
                ]
            else:
                self.storyPanel.findWidget(
                    'StoryIncludePassages').passage.tags[:] = ['Twine.hide']
                self.storyPanel.findWidget(
                    'StoryIncludePassages').passage.tags.append('Twine.system')
                self.storyPanel.findWidget('StoryIncludePassages').pos = [
                    -1000, -1000
                ]
            # assemble our tiddlywiki and write it out
            hasstartpassage = False
            tw = TiddlyWiki()
            for widget in self.storyPanel.widgets:
                # if widget.passage.title != 'StoryIncludes' and \
                # not any('Twine.private' in t for t in widget.passage.tags) and \
                # not any('Twine.system' in t for t in widget.passage.tags):
                if widget.passage.title != 'StoryIncludes' and \
                not any(t.startswith('Twine.') for t in widget.passage.tags):
                    tw.addTiddler(widget.passage)
                    if widget.passage.title == "Start":
                        hasstartpassage = True

            # is there a Start passage?
            if hasstartpassage == False:
                self.app.displayError(
                    'building your story because there is no "Start" passage. '
                    + "\n" +
                    'Your story will build but the web-browser will not be able to run the story. '
                    + "\n" + 'Please add a passage with the title "Start"')

            for widget in self.storyPanel.widgets:
                if widget.passage.title == 'StoryIncludes':
                    lines = widget.passage.text.splitlines()
                    lines.append('')
                    # State 0: Look for a filename
                    ## State 1: have filename, look for filename, EXCEPT, INCLUDE, ALIAS
                    ## State 2: EXCEPT mode, look for INCLUDE 3, ALIAS 4 or blank line 0
                    ## State 3: INCLUDE mode, look for EXCEPT 2, ALIAS 4 or blank line 0
                    ## State 4: ALIAS mode, look for EXCEPT 2, INCLUDE 2 or blank line 0
                    state = 0
                    state_filename = ''
                    excludepassages = [
                        'Start', 'StoryMenu', 'StoryTitle', 'StoryAuthor',
                        'StorySubtitle', 'StoryIncludes', 'StorySettings'
                    ]
                    for line in lines:
                        if state == 0:
                            state_filename = line
                            state = 1
                            continue
                        elif state == 1:
                            try:
                                if state_filename.strip() != '':
                                    extension = os.path.splitext(
                                        state_filename)[1]
                                    if extension == '.tws':
                                        if any(
                                                state_filename.startswith(t)
                                                for t in
                                            ['http://', 'https://', 'ftp://']):
                                            openedFile = urllib.urlopen(
                                                state_filename)
                                        else:
                                            openedFile = open(
                                                state_filename, 'r')
                                        s = StoryFrame(
                                            None,
                                            app=self.app,
                                            state=pickle.load(openedFile))
                                        openedFile.close()
                                        for widget in s.storyPanel.widgets:
                                            if not any(widget.passage.title in t for t in excludepassages) and \
                                            not any('Twine.private' in t for t in widget.passage.tags) and \
                                            not any('Twine.system' in t for t in widget.passage.tags):
                                                tw.addTiddler(widget.passage)
                                                if self.storyPanel.findWidget(
                                                        'StoryIncludePassages'
                                                ):
                                                    self.storyPanel.findWidget(
                                                        'StoryIncludePassages'
                                                    ).passage.tags.append(
                                                        widget.passage.title)
                                        s.Destroy()
                                    elif extension == '.tw' or extension == '.txt' or extension == '.twee':
                                        if any(
                                                state_filename.startswith(t)
                                                for t in
                                            ['http://', 'https://', 'ftp://']):
                                            openedFile = urllib.urlopen(
                                                state_filename)
                                            s = openedFile.read()
                                            openedFile.close()
                                            t = tempfile.NamedTemporaryFile(
                                                delete=False)
                                            cleanuptempfile = True
                                            t.write(s)
                                            t.close()
                                            filename = t.name
                                        else:
                                            filename = state_filename
                                            cleanuptempfile = False

                                        tw1 = TiddlyWiki()
                                        tw1.addTweeFromFilename(filename)
                                        if cleanuptempfile: os.remove(filename)
                                        tiddlerkeys = tw1.tiddlers.keys()
                                        for tiddlerkey in tiddlerkeys:
                                            passage = tw1.tiddlers[tiddlerkey]
                                            if not any(passage.title == t for t in excludepassages) and \
                                            not any('Twine.private' in t for t in passage.tags) and \
                                            not any('Twine.system' in t for t in passage.tags):
                                                tw.addTiddler(passage)
                                                if self.storyPanel.findWidget(
                                                        'StoryIncludePassages'
                                                ):
                                                    self.storyPanel.findWidget(
                                                        'StoryIncludePassages'
                                                    ).passage.tags.append(
                                                        passage.title)
                                    else:
                                        raise 'File format not recognized'
                            except:
                                self.app.displayError(
                                    'opening the Twine file named ' +
                                    state_filename +
                                    ' which is referred to by the passage StoryIncludes'
                                )
                            state_filename = line
                            state = 1
                            continue
                    break

            # Decode story settings
            for widget in self.storyPanel.widgets:
                if widget.passage.title == 'StorySettings':
                    lines = widget.passage.text.splitlines()
                    for line in lines:
                        (skey, svalue) = line.split(':')
                        tw.storysettings[skey.strip()] = svalue.strip()
                    break

            # Write the output file
            os.chdir(os.path.dirname(self.buildDestination))
            dest = open(self.buildDestination, 'w')
            dest.write(
                tw.toHtml(self.app, self.target,
                          savePath=self.saveDestination).encode('utf-8'))
            dest.close()
            os.chdir(cwd)
            if displayAfter: self.viewBuild()
        except:
            self.app.displayError('building your story')

    def viewBuild(self, event=None):
        """
        Opens the last built file in a Web browser.
        """
        path = 'file://' + urllib.pathname2url(self.buildDestination)
        path = path.replace('file://///', 'file:///')
        wx.LaunchDefaultBrowser(path)

    def autoBuild(self, event=None):
        """
        Toggles the autobuild feature
        """
        if self.autobuildmenuitem.IsChecked():
            self.autobuildtimer.Start(5000)
            self.autoBuildStart()
        else:
            self.autobuildtimer.Stop()

    def autoBuildTick(self, event=None):
        """
        Called whenever the autobuild timer checks up on things
        """
        for pathname, oldmtime in self.autobuildfiles.iteritems():
            newmtime = os.stat(pathname).st_mtime
            if newmtime != oldmtime:
                #print "Auto rebuild triggered by: ", pathname
                self.autobuildfiles[pathname] = newmtime
                self.rebuild()
                break

    def autoBuildStart(self):
        self.autobuildfiles = {}
        if self.saveDestination == '':
            twinedocdir = cwd
        else:
            twinedocdir = os.path.dirname(self.saveDestination)
        for f in os.listdir(twinedocdir):
            extension = os.path.splitext(f)[1]
            if extension == '.tws' or extension == '.tw' or extension == '.txt' or extension == '.twee':
                pathname = os.path.join(twinedocdir, f)
                mtime = os.stat(pathname).st_mtime
                self.autobuildfiles[pathname] = mtime

    def stats(self, event=None):
        """
        Displays a StatisticsDialog for this frame.
        """

        statFrame = StatisticsDialog(parent=self,
                                     storyPanel=self.storyPanel,
                                     app=self.app)
        statFrame.ShowModal()

    def showFind(self, event=None):
        """
        Shows a StoryFindFrame for this frame.
        """

        if (not hasattr(self, 'findFrame')):
            self.findFrame = StoryFindFrame(self.storyPanel, self.app)
        else:
            try:
                self.findFrame.Raise()
            except wx._core.PyDeadObjectError:
                # user closed the frame, so we need to recreate it
                delattr(self, 'findFrame')
                self.showFind(event)

    def showReplace(self, event=None):
        """
        Shows a StoryReplaceFrame for this frame.
        """
        if (not hasattr(self, 'replaceFrame')):
            self.replaceFrame = StoryReplaceFrame(self.storyPanel, self.app)
        else:
            try:
                self.replaceFrame.Raise()
            except wx._core.PyDeadObjectError:
                # user closed the frame, so we need to recreate it
                delattr(self, 'replaceFrame')
                self.showReplace(event)

    def proof(self, event=None):
        """
        Builds an RTF version of the story. Pass whether to open the destination file afterwards.
        """

        # ask for our destination

        dialog = wx.FileDialog(self, 'Proof Story', os.getcwd(), "", \
                         "RTF Document (*.rtf)|*.rtf", \
                           wx.SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR)

        if dialog.ShowModal() == wx.ID_OK:
            path = dialog.GetPath()
            dialog.Destroy()
        else:
            dialog.Destroy()
            return

        try:
            # open destination for writing

            dest = open(path, 'w')

            # assemble our tiddlywiki and write it out

            tw = TiddlyWiki()
            for widget in self.storyPanel.sortedWidgets():
                tw.addTiddler(widget.passage)

            order = map(lambda w: w.passage.title,
                        self.storyPanel.sortedWidgets())
            dest.write(tw.toRtf(order))
            dest.close()
        except:
            self.app.displayError('building a proofing copy of your story')

    def setTarget(self, target):
        self.target = target

    def updateUI(self, event=None):
        """Adjusts menu items to reflect the current state."""

        hasSelection = self.storyPanel.hasSelection()

        canPaste = False
        if wx.TheClipboard.Open():
            canPaste = wx.TheClipboard.IsSupported(
                wx.CustomDataFormat(StoryPanel.CLIPBOARD_FORMAT))
            wx.TheClipboard.Close()

        # window title

        if self.saveDestination == '':
            title = StoryFrame.DEFAULT_TITLE
        else:
            bits = os.path.splitext(self.saveDestination)
            title = os.path.basename(bits[0])

        percent = str(int(round(self.storyPanel.scale * 100)))
        dirty = ''
        if self.dirty: dirty = ' *'

        self.SetTitle(title + dirty + ' (' + percent + '%) ' + '- ' +
                      self.app.NAME)

        # File menu

        revertItem = self.menus.FindItemById(wx.ID_REVERT_TO_SAVED)
        revertItem.Enable(self.saveDestination != '' and self.dirty)

        # Edit menu

        undoItem = self.menus.FindItemById(wx.ID_UNDO)
        undoItem.Enable(self.storyPanel.canUndo())
        if self.storyPanel.canUndo():
            undoItem.SetText('Undo ' + self.storyPanel.undoAction() +
                             '\tCtrl-Z')
        else:
            undoItem.SetText("Can't Undo\tCtrl-Z")

        redoItem = self.menus.FindItemById(wx.ID_REDO)
        redoItem.Enable(self.storyPanel.canRedo())
        if self.storyPanel.canRedo():
            redoItem.SetText('Redo ' + self.storyPanel.redoAction() +
                             '\tCtrl-Y')
        else:
            redoItem.SetText("Can't Redo\tCtrl-Y")

        cutItem = self.menus.FindItemById(wx.ID_CUT)
        cutItem.Enable(hasSelection)
        copyItem = self.menus.FindItemById(wx.ID_COPY)
        copyItem.Enable(hasSelection)
        deleteItem = self.menus.FindItemById(wx.ID_DELETE)
        deleteItem.Enable(hasSelection)
        pasteItem = self.menus.FindItemById(wx.ID_PASTE)
        pasteItem.Enable(canPaste)

        findAgainItem = self.menus.FindItemById(StoryFrame.EDIT_FIND_NEXT)
        findAgainItem.Enable(self.storyPanel.lastSearchRegexp != None)

        # View menu

        toolbarItem = self.menus.FindItemById(StoryFrame.VIEW_TOOLBAR)
        toolbarItem.Check(self.showToolbar)
        snapItem = self.menus.FindItemById(StoryFrame.VIEW_SNAP)
        snapItem.Check(self.storyPanel.snapping)

        # Story menu

        editItem = self.menus.FindItemById(wx.ID_EDIT)
        editItem.Enable(hasSelection)

        editFullscreenItem = self.menus.FindItemById(
            StoryFrame.STORY_EDIT_FULLSCREEN)
        editFullscreenItem.Enable(
            hasSelection and not self.storyPanel.hasMultipleSelection())

        rebuildItem = self.menus.FindItemById(StoryFrame.STORY_REBUILD)
        rebuildItem.Enable(self.buildDestination != '')

        viewLastItem = self.menus.FindItemById(StoryFrame.STORY_VIEW_LAST)
        viewLastItem.Enable(self.buildDestination != '')

        # Story format submenu

        for key in self.storyFormats:
            self.menus.FindItemById(key).Check(
                self.target == self.storyFormats[key])

    def toggleToolbar(self, event=None):
        """Toggles the toolbar onscreen."""
        if (self.showToolbar):
            self.showToolbar = False
            self.toolbar.Hide()
            self.app.config.WriteBool('storyFrameToolbar', False)
        else:
            self.showToolbar = True
            self.toolbar.Show()
            self.app.config.WriteBool('storyFrameToolbar', True)
        self.SendSizeEvent()

    def setDirty(self, value, action=None):
        """
        Sets the dirty flag to the value passed. Make sure to use this instead of
        setting the dirty property directly, as this method automatically updates
        the pristine property as well.
        
        If you pass an action parameter, this action will be saved for undoing under
        that name.
        """
        self.dirty = value
        self.pristine = False

        if value is True and action:
            self.storyPanel.pushUndo(action)

    def applyPrefs(self):
        """Passes on the apply message to child widgets."""
        self.storyPanel.eachWidget(lambda w: w.applyPrefs())
        self.storyPanel.Refresh()

    def serialize(self):
        """Returns a dictionary of state suitable for pickling."""
        return { 'target': self.target, 'buildDestination': self.buildDestination, \
                 'saveDestination': self.saveDestination, \
                 'storyPanel': self.storyPanel.serialize() }

    def serialize_noprivate(self, dest):
        """Returns a dictionary of state suitable for pickling."""
        return { 'target': self.target, 'buildDestination': '', \
                 'saveDestination': dest, \
                 'storyPanel': self.storyPanel.serialize_noprivate() }

    def __repr__(self):
        return "<StoryFrame '" + self.saveDestination + "'>"

    # menu constants
    # (that aren't already defined by wx)

    FILE_PAGE_SETUP = 101  # release 3 :)
    FILE_PRINT = 102  # release 3
    FILE_IMPORT_SOURCE = 103
    FILE_EXPORT_PROOF = 104
    FILE_EXPORT_SOURCE = 105

    EDIT_FIND_NEXT = 201

    VIEW_SNAP = 301
    VIEW_CLEANUP = 302
    VIEW_TOOLBAR = 303

    STORY_NEW_PASSAGE = 401
    STORY_EDIT_FULLSCREEN = 402
    STORY_BUILD = 403
    STORY_REBUILD = 404
    STORY_VIEW_LAST = 405
    STORY_AUTO_BUILD = 406
    STORY_STATS = 407

    STORY_FORMAT_HELP = 408
    STORY_FORMAT_BASE = 409

    HELP_MANUAL = 501
    HELP_GROUP = 502
    HELP_BUG = 503

    # tooltip labels

    NEW_PASSAGE_TOOLTIP = 'Add a new passage to your story'
    ZOOM_IN_TOOLTIP = 'Zoom in'
    ZOOM_OUT_TOOLTIP = 'Zoom out'
    ZOOM_FIT_TOOLTIP = 'Zoom so all passages are visible onscreen'
    ZOOM_ONE_TOOLTIP = 'Zoom to 100%'

    # size constants

    DEFAULT_SIZE = (800, 600)
    TOOLBAR_ICON_SIZE = 32

    # misc stuff

    DEFAULT_TITLE = 'Untitled Story'