import sys, re, os, urllib, urlparse, pickle, wx, codecs, tempfile, images, version from wx.lib import imagebrowser from tiddlywiki import TiddlyWiki from storypanel import StoryPanel from passagewidget import PassageWidget from statisticsdialog import StatisticsDialog from storysearchframes import StoryFindFrame, StoryReplaceFrame from storymetadataframe import StoryMetadataFrame from utils import isURL class StoryFrame(wx.Frame): """ A StoryFrame displays an entire story. Its main feature is an instance of a StoryPanel, but it also has a menu bar and toolbar. """ def __init__(self, parent, app, state=None, refreshIncludes=True): 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 self.lastTestBuild = None self.title = "" # inner state if state: self.buildDestination = state.get('buildDestination', '') self.saveDestination = state.get('saveDestination', '') self.setTarget(state.get('target', 'sugarcane').lower()) self.metadata = state.get('metadata', {}) self.storyPanel = StoryPanel(self, app, state=state['storyPanel']) self.pristine = False else: self.buildDestination = '' self.saveDestination = '' self.metadata = {} self.setTarget('sugarcane') self.storyPanel = StoryPanel(self, app) if refreshIncludes: self.storyPanel.refreshIncludedPassageList() # 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_HTML, 'Compiled &HTML File...') self.Bind(wx.EVT_MENU, self.importHtmlDialog, id=StoryFrame.FILE_IMPORT_HTML) importMenu.Append(StoryFrame.FILE_IMPORT_SOURCE, 'Twee Source &Code...') self.Bind(wx.EVT_MENU, self.importSourceDialog, id=StoryFrame.FILE_IMPORT_SOURCE) 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.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 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() # New Passage submenu self.newPassageMenu = wx.Menu() self.newPassageMenu.Append(StoryFrame.STORY_NEW_PASSAGE, '&Passage\tCtrl-N') self.Bind(wx.EVT_MENU, self.storyPanel.newWidget, id=StoryFrame.STORY_NEW_PASSAGE) self.newPassageMenu.AppendSeparator() self.newPassageMenu.Append(StoryFrame.STORY_NEW_STYLESHEET, 'S&tylesheet') self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.newWidget(text=self.storyPanel.FIRST_CSS, \ tags=['stylesheet']), id=StoryFrame.STORY_NEW_STYLESHEET) self.newPassageMenu.Append(StoryFrame.STORY_NEW_SCRIPT, '&Script') self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.newWidget(tags=['script']), id=StoryFrame.STORY_NEW_SCRIPT) self.newPassageMenu.Append(StoryFrame.STORY_NEW_ANNOTATION, '&Annotation') self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.newWidget(tags=['annotation']), id=StoryFrame.STORY_NEW_ANNOTATION) self.storyMenu.AppendMenu(wx.ID_ANY, 'New', self.newPassageMenu) 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 in &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.AppendSeparator() self.importImageMenu = wx.Menu() self.importImageMenu.Append(StoryFrame.STORY_IMPORT_IMAGE, 'From &File...') self.Bind(wx.EVT_MENU, self.importImageDialog, id=StoryFrame.STORY_IMPORT_IMAGE) self.importImageMenu.Append(StoryFrame.STORY_IMPORT_IMAGE_URL, 'From Web &URL...') self.Bind(wx.EVT_MENU, self.importImageURLDialog, id=StoryFrame.STORY_IMPORT_IMAGE_URL) self.storyMenu.AppendMenu(wx.ID_ANY, 'Import &Image', self.importImageMenu) self.storyMenu.Append(StoryFrame.STORY_IMPORT_FONT, 'Import &Font...') self.Bind(wx.EVT_MENU, self.importFontDialog, id=StoryFrame.STORY_IMPORT_FONT) self.storyMenu.AppendSeparator() # Story Settings submenu self.storySettingsMenu = wx.Menu() self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_START, 'Start') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_START) self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_TITLE, 'StoryTitle') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_TITLE) self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_SUBTITLE, 'StorySubtitle') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_SUBTITLE) self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_AUTHOR, 'StoryAuthor') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_AUTHOR) self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_MENU, 'StoryMenu') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_MENU) self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_INIT, 'StoryInit') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_INIT) # Separator for 'visible' passages (title, subtitle) and those that solely affect compilation self.storySettingsMenu.AppendSeparator() self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_SETTINGS, 'StorySettings') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_SETTINGS) self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_INCLUDES, 'StoryIncludes') self.Bind(wx.EVT_MENU, self.createInfoPassage, id=StoryFrame.STORYSETTINGS_INCLUDES) self.storySettingsMenu.AppendSeparator() self.storySettingsMenu.Append(StoryFrame.STORYSETTINGS_HELP, 'About Special Passages') self.Bind(wx.EVT_MENU, lambda e: wx.LaunchDefaultBrowser('http://twinery.org/wiki/special_passages'), id=StoryFrame.STORYSETTINGS_HELP) self.storyMenu.AppendMenu(wx.ID_ANY, 'Special Passages', self.storySettingsMenu) self.storyMenu.AppendSeparator() self.storyMenu.Append(StoryFrame.REFRESH_INCLUDES_LINKS, 'Update StoryIncludes Links') self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.refreshIncludedPassageList(), id=StoryFrame.REFRESH_INCLUDES_LINKS) self.storyMenu.AppendSeparator() # Story Format submenu storyFormatMenu = wx.Menu() storyFormatCounter = StoryFrame.STORY_FORMAT_BASE for key in sorted(app.headers.keys()): header = app.headers[key] storyFormatMenu.Append(storyFormatCounter, header.label, kind=wx.ITEM_CHECK) self.Bind(wx.EVT_MENU, lambda e, target=key: self.setTarget(target), id=storyFormatCounter) self.storyFormats[storyFormatCounter] = header storyFormatCounter += 1 if storyFormatCounter: 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) self.storyMenu.Append(StoryFrame.STORY_METADATA, 'Story &Metadata...') self.Bind(wx.EVT_MENU, self.showMetadata, id=StoryFrame.STORY_METADATA) self.storyMenu.Append(StoryFrame.STORY_STATS, 'Story &Statistics\tCtrl-I') self.Bind(wx.EVT_MENU, self.stats, id=StoryFrame.STORY_STATS) # Build menu buildMenu = wx.Menu() buildMenu.Append(StoryFrame.BUILD_TEST, '&Test Play\tCtrl-T') self.Bind(wx.EVT_MENU, self.testBuild, id=StoryFrame.BUILD_TEST) buildMenu.Append(StoryFrame.BUILD_TEST_HERE, 'Test Play From Here\tCtrl-Shift-T') self.Bind(wx.EVT_MENU, lambda e: self.storyPanel.eachSelectedWidget(lambda w: self.testBuild(startAt=w.passage.title)), \ id=StoryFrame.BUILD_TEST_HERE) buildMenu.Append(StoryFrame.BUILD_VERIFY, '&Verify All Passages') self.Bind(wx.EVT_MENU, self.verify, id=StoryFrame.BUILD_VERIFY) buildMenu.AppendSeparator() buildMenu.Append(StoryFrame.BUILD_BUILD, '&Build Story...\tCtrl-B') self.Bind(wx.EVT_MENU, self.build, id=StoryFrame.BUILD_BUILD) buildMenu.Append(StoryFrame.BUILD_REBUILD, '&Rebuild Story\tCtrl-R') self.Bind(wx.EVT_MENU, self.rebuild, id=StoryFrame.BUILD_REBUILD) buildMenu.Append(StoryFrame.BUILD_VIEW_LAST, '&Rebuild and View\tCtrl-L') self.Bind(wx.EVT_MENU, lambda e: self.rebuild(displayAfter=True), id=StoryFrame.BUILD_VIEW_LAST) buildMenu.AppendSeparator() self.autobuildmenuitem = buildMenu.Append(StoryFrame.BUILD_AUTO_BUILD, '&Auto Build', kind=wx.ITEM_CHECK) self.Bind(wx.EVT_MENU, self.autoBuild, self.autobuildmenuitem) buildMenu.Check(StoryFrame.BUILD_AUTO_BUILD, False) # Help menu helpMenu = wx.Menu() helpMenu.Append(StoryFrame.HELP_MANUAL, 'Twine &Wiki') self.Bind(wx.EVT_MENU, self.app.openDocs, id=StoryFrame.HELP_MANUAL) helpMenu.Append(StoryFrame.HELP_FORUM, 'Twine &Forum') self.Bind(wx.EVT_MENU, self.app.openForum, id=StoryFrame.HELP_FORUM) helpMenu.Append(StoryFrame.HELP_GITHUB, 'Twine\'s Source Code 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(buildMenu, '&Build') self.menus.Append(helpMenu, '&Help') self.SetMenuBar(self.menus) # enable/disable paste menu option depending on clipboard contents self.clipboardMonitor = ClipboardMonitor(self.menus.FindItemById(wx.ID_PASTE).Enable) self.clipboardMonitor.Start(100) # 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) \ ])) iconPath = self.app.iconsPath 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.widgetDict.itervalues()): if isinstance(w, PassageWidget): w.closeEditor() if self.lastTestBuild and os.path.exists(self.lastTestBuild.name): try: os.remove(self.lastTestBuild.name) except OSError, ex: print >> sys.stderr, 'Failed to remove lastest test build:', ex self.lastTestBuild = None self.app.removeStory(self, byMenu) if event is not 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.FD_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.FD_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.widgetDict.itervalues(): tw.addTiddler(widget.passage) dest = codecs.open(path, 'w', 'utf-8-sig', 'replace') order = [widget.passage.title for widget in self.storyPanel.sortedWidgets()] dest.write(tw.toTwee(order)) dest.close() except: self.app.displayError('exporting your source code') dialog.Destroy() def importHtmlDialog(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.FD_OPEN | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: self.importHtml(dialog.GetPath()) def importHtml(self, path): """Imports the tiddler objects in a HTML file into the story.""" self.importSource(path, True) def importSourceDialog(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.FD_OPEN | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: self.importSource(dialog.GetPath()) def importSource(self, path, html=False): """Imports the tiddler objects in a Twee file into the story.""" try: # have a TiddlyWiki object parse it for us tw = TiddlyWiki() if html: tw.addHtmlFromFilename(path) else: tw.addTweeFromFilename(path) # add passages for each of the tiddlers the TiddlyWiki saw if len(tw.tiddlers): removedWidgets = [] skippedTitles = set() # Ask user how to resolve any passage title conflicts for title in tw.tiddlers.viewkeys() & self.storyPanel.widgetDict.viewkeys(): dialog = wx.MessageDialog(self, 'There is already a passage titled "' + title \ + '" in this story. Replace it with the imported passage?', 'Passage Title Conflict', \ wx.ICON_WARNING | wx.YES_NO | wx.CANCEL | wx.YES_DEFAULT) check = dialog.ShowModal() if check == wx.ID_YES: removedWidgets.append(title) elif check == wx.ID_CANCEL: return elif check == wx.ID_NO: skippedTitles.add(title) # Remove widgets elected to be replaced for title in removedWidgets: self.storyPanel.removeWidget(title) # Insert widgets now lastpos = [0, 0] addedWidgets = [] for tiddler in tw.tiddlers.itervalues(): if tiddler.title in skippedTitles: continue new = self.storyPanel.newWidget(title=tiddler.title, tags=tiddler.tags, text=tiddler.text, quietly=True, pos=tiddler.pos if tiddler.pos else lastpos) lastpos = new.pos addedWidgets.append(new) self.setDirty(True, 'Import') for widget in addedWidgets: widget.clearPaintCache() else: if html: what = "compiled HTML" else: what = "Twee source" dialog = wx.MessageDialog(self, 'No passages were found in this file. Make sure ' + \ 'this is a ' + what + ' file.', 'No Passages Found', \ wx.ICON_INFORMATION | wx.OK) dialog.ShowModal() except: self.app.displayError('importing') def importImageURL(self, url, showdialog=True): """ Downloads the image file from the url and creates a passage. Returns the resulting passage name, or None """ try: # Download the file urlfile = urllib.urlopen(url) path = urlparse.urlsplit(url)[2] title = os.path.splitext(os.path.basename(path))[0] file = urlfile.read().encode('base64').replace('\n', '') # Now that the file's read, check the info maintype = urlfile.info().getmaintype() if maintype != "image": self.app.displayError("importing from the web: The server served " + maintype + " instead of an image", stacktrace=False) return None # Convert the file mimeType = urlfile.info().gettype() urlfile.close() text = "data:" + mimeType + ";base64," + file return self.finishImportImage(text, title, showdialog=showdialog) except: self.app.displayError('importing from the web') return None def importImageURLDialog(self, event=None): dialog = wx.TextEntryDialog(self, "Enter the image URL (GIFs, JPEGs, PNGs, SVGs and WebPs only)", "Import Image from Web", "http://") if dialog.ShowModal() == wx.ID_OK: self.importImageURL(dialog.GetValue()) def importImageFile(self, file, replace=None, showdialog=True): """ Perform the file I/O to import an image file, then add it as an image passage. Returns the name of the resulting passage, or None """ try: if not replace: text, title = self.openFileAsBase64(file) return self.finishImportImage(text, title, showdialog=showdialog) else: replace.passage.text = self.openFileAsBase64(file)[0] replace.updateBitmap() return replace.passage.title except IOError: self.app.displayError('importing an image') return None def importImageDialog(self, event=None, useImageDialog=False, replace=None): """Asks the user to choose an image file to import, then imports into the current story. replace is a Tiddler, if any, that will be replaced by the image.""" # Use the wxPython image browser? if useImageDialog: dialog = imagebrowser.ImageDialog(self, os.getcwd()) dialog.ChangeFileTypes([('Web Image File', '*.(gif|jpg|jpeg|png|webp|svg)')]) dialog.ResetFiles() else: dialog = wx.FileDialog(self, 'Import Image File', os.getcwd(), '', \ 'Web Image File|*.gif;*.jpg;*.jpeg;*.png;*.webp;*.svg|All Files (*.*)|*.*', wx.FD_OPEN | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: file = dialog.GetFile() if useImageDialog else dialog.GetPath() self.importImageFile(file, replace) def importFontDialog(self, event=None): """Asks the user to choose a font file to import, then imports into the current story.""" dialog = wx.FileDialog(self, 'Import Font File', os.getcwd(), '', \ 'Web Font File (.ttf, .otf, .woff, .svg)|*.ttf;*.otf;*.woff;*.svg|All Files (*.*)|*.*', wx.FD_OPEN | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: self.importFont(dialog.GetPath()) def openFileAsBase64(self, file): """Opens a file and returns its base64 representation, expressed as a Data URI with MIME type""" file64 = open(file, 'rb').read().encode('base64').replace('\n', '') title, mimeType = os.path.splitext(os.path.basename(file)) return (images.addURIPrefix(file64, mimeType[1:]), title) def newTitle(self, title): """ Check if a title is being used, and increment its number if it is.""" while self.storyPanel.passageExists(title): try: match = re.search(r'(\s\d+)$', title) if match: title = title[:match.start(1)] + " " + str(int(match.group(1)) + 1) else: title += " 2" except: pass return title def finishImportImage(self, text, title, showdialog=True): """Imports an image into the story as an image passage.""" # Check for title usage title = self.newTitle(title) self.storyPanel.newWidget(text=text, title=title, tags=['Twine.image']) if showdialog: dialog = wx.MessageDialog(self, 'Image file imported successfully.\n' + \ 'You can include the image in your passages with this syntax:\n\n' + \ '[img[' + title + ']]', 'Image added', \ wx.ICON_INFORMATION | wx.OK) dialog.ShowModal() return title def importFont(self, file, showdialog=True): """Imports a font into the story as a font passage.""" try: text, title = self.openFileAsBase64(file) title2 = self.newTitle(title) # Wrap in CSS @font-face declaration text = \ """font[face=\"""" + title + """\"] { font-family: \"""" + title + """\"; } @font-face { font-family: \"""" + title + """\"; src: url(""" + text + """); }""" self.storyPanel.newWidget(text=text, title=title2, tags=['stylesheet']) if showdialog: dialog = wx.MessageDialog(self, 'Font file imported successfully.\n' + \ 'You can use the font in your stylesheets with this CSS attribute syntax:\n\n' + \ 'font-family: ' + title + ";", 'Font added', \ wx.ICON_INFORMATION | wx.OK) dialog.ShowModal() return True except IOError: self.app.displayError('importing a font') return False def defaultTextForPassage(self, title): if title == 'Start': return "Your story will display this passage first. Edit it by double clicking it." elif title == 'StoryTitle': return self.DEFAULT_TITLE elif title == 'StorySubtitle': return "This text appears below the story's title." elif title == 'StoryAuthor': return "Anonymous" elif title == 'StoryMenu': return "This passage's text will be included in the menu for this story." elif title == 'StoryInit': return """/% Place your story's setup code in this passage. Any macros in this passage will be run before the Start passage (or any passage you wish to Test Play) is run. %/ """ elif title == 'StoryIncludes': return """List the file paths of any .twee or .tws files that should be merged into this story when it's built. You can also include URLs of .tws and .twee files, too. """ else: return "" def createInfoPassage(self, event): """Open an editor for a special passage; create it if it doesn't exist yet.""" id = event.GetId() title = self.storySettingsMenu.FindItemById(id).GetLabel() # What to do about StoryIncludes files? editingWidget = self.storyPanel.findWidget(title) if editingWidget is None: editingWidget = self.storyPanel.newWidget(title=title, text=self.defaultTextForPassage(title)) editingWidget.openEditor() 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 verify(self, event=None): """Runs the syntax checks on all passages.""" noprobs = True for widget in self.storyPanel.widgetDict.itervalues(): result = widget.verifyPassage(self) if result == -1: break elif result > 0: noprobs = False if noprobs: wx.MessageDialog(self, "No obvious problems found in " + str( len(self.storyPanel.widgetDict)) + " passage" + ( "s." if len(self.storyPanel.widgetDict) > 1 else ".") \ + "\n\n(There may still be problems when the story is played, of course.)", "Verify All Passages", wx.ICON_INFORMATION).ShowModal() def build(self, event=None): """Asks the user to choose a location to save a compiled story, then passed control to rebuild().""" path, filename = os.path.split(self.buildDestination) dialog = wx.FileDialog(self, 'Build Story', path or os.getcwd(), filename, \ "Web Page (*.html)|*.html", \ wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: self.buildDestination = dialog.GetPath() self.rebuild(None, displayAfter=True) dialog.Destroy() def testBuild(self, event=None, startAt=''): self.rebuild(temp=True, startAt=startAt, displayAfter=True) def rebuild(self, event=None, temp=False, displayAfter=False, startAt=''): """ Builds an HTML version of the story. Pass whether to use a temp file, and/or open the file afterwards. """ try: # assemble our tiddlywiki and write it out hasstartpassage = False tw = TiddlyWiki() for widget in self.storyPanel.widgetDict.itervalues(): if widget.passage.title == 'StoryIncludes': def callback(passage, tw=tw): if passage.title == 'StoryIncludes': return # Check for uniqueness elif passage.title in self.storyPanel.widgetDict: # Not bothering with a Yes/No dialog here. raise Exception('A passage titled "' + passage.title + '" is already present in this story') elif tw.hasTiddler(passage.title): raise Exception( 'A passage titled "' + passage.title + '" has been included by a previous StoryIncludes file') tw.addTiddler(passage) self.storyPanel.addIncludedPassage(passage.title) self.readIncludes(widget.passage.text.splitlines(), callback) # Might as well suppress the warning for a StoryIncludes file hasstartpassage = True elif TiddlyWiki.NOINCLUDE_TAGS.isdisjoint(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"') widget = self.storyPanel.widgetDict.get('StorySettings') if widget is not None: lines = widget.passage.text.splitlines() for line in lines: if ':' in line: (skey, svalue) = line.split(':') skey = skey.strip().lower() svalue = svalue.strip() tw.storysettings[skey] = svalue # Write the output file header = self.app.headers.get(self.target) metadata = self.metadata if temp: # This implicitly closes the previous test build if self.lastTestBuild and os.path.exists(self.lastTestBuild.name): os.remove(self.lastTestBuild.name) path = (os.path.exists(self.buildDestination) and self.buildDestination) \ or (os.path.exists(self.saveDestination) and self.saveDestination) or None html = tw.toHtml(self.app, header, startAt=startAt, defaultName=self.title, metadata=metadata) if html: self.lastTestBuild = tempfile.NamedTemporaryFile(mode='wb', suffix=".html", delete=False, dir=(path and os.path.dirname(path)) or None) self.lastTestBuild.write(html.encode('utf-8-sig')) self.lastTestBuild.close() if displayAfter: self.viewBuild(name=self.lastTestBuild.name) else: dest = open(self.buildDestination, 'wb') dest.write(tw.toHtml(self.app, header, defaultName=self.title, metadata=metadata).encode('utf-8-sig')) dest.close() if displayAfter: self.viewBuild() except: self.app.displayError('building your story') def getLocalDir(self): dir = (self.saveDestination != '' and os.path.dirname(self.saveDestination)) or None if not (dir and os.path.isdir(dir)): dir = os.getcwd() return dir def readIncludes(self, lines, callback, silent=False): """ Examines all of the source files included via StoryIncludes, and performs a callback on each passage found. callback is a function that takes 1 Tiddler object. """ twinedocdir = self.getLocalDir() excludetags = TiddlyWiki.NOINCLUDE_TAGS self.storyPanel.clearIncludedPassages() for line in lines: try: if line.strip(): extension = os.path.splitext(line)[1] if extension not in ['.tws', '.tw', '.txt', '.twee']: raise Exception('File format not recognized') if isURL(line): openedFile = urllib.urlopen(line) else: openedFile = open(os.path.join(twinedocdir, line), 'r') if extension == '.tws': s = StoryFrame(None, app=self.app, state=pickle.load(openedFile), refreshIncludes=False) openedFile.close() for widget in s.storyPanel.widgetDict.itervalues(): if excludetags.isdisjoint(widget.passage.tags): callback(widget.passage) s.Destroy() else: s = openedFile.read() openedFile.close() tw1 = TiddlyWiki() tw1.addTwee(s) tiddlerkeys = tw1.tiddlers.keys() for tiddlerkey in tiddlerkeys: passage = tw1.tiddlers[tiddlerkey] if excludetags.isdisjoint(passage.tags): callback(passage) except: if not silent: self.app.displayError( 'reading the file named "' + line + '" which is referred to by the StoryIncludes passage', stacktrace=False) def viewBuild(self, event=None, name=''): """ Opens the last built file in a Web browser. """ path = u'file://' + urllib.pathname2url((name or self.buildDestination).encode('utf-8')) 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 = os.getcwd() else: twinedocdir = os.path.dirname(self.saveDestination) widget = self.storyPanel.widgetDict.get('StoryIncludes') if widget is not None: for line in widget.passage.text.splitlines(): if not isURL(line): pathname = os.path.join(twinedocdir, line) # Include even non-existant files, in case they eventually appear 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 showMetadata(self, event=None): """ Shows a StoryMetadataFrame for this frame. """ if not hasattr(self, 'metadataFrame'): self.metadataFrame = StoryMetadataFrame(parent=self, app=self.app) else: try: self.metadataFrame.Raise() except wx._core.PyDeadObjectError: # user closed the frame, so we need to recreate it delattr(self, 'metadataFrame') self.showMetadata(event) 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.FD_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(): # Exclude images from RTF, they appear as large unreadable blobs of base64 text. if 'Twine.image' not in widget.passage.tags: tw.addTiddler(widget.passage) order = [widget.passage.title for widget in 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 self.header = self.app.headers[target] def updateUI(self, event=None): """Adjusts menu items to reflect the current state.""" selections = self.storyPanel.hasMultipleSelection() # window title if self.saveDestination == '': self.title = StoryFrame.DEFAULT_TITLE else: bits = os.path.splitext(self.saveDestination) self.title = os.path.basename(bits[0]) percent = str(int(round(self.storyPanel.scale * 100))) dirtyText = '' if not self.dirty else ' *' titleText = self.title + dirtyText + ' (' + percent + '%) ' + '- ' + self.app.NAME + ' ' + version.versionString if not self.GetTitle() == titleText: self.SetTitle(titleText) if not self.menus: return # File menu self.menus.FindItemById(wx.ID_REVERT_TO_SAVED).Enable(self.saveDestination != '' and self.dirty) # Edit menu undoItem = self.menus.FindItemById(wx.ID_UNDO) undoItem.Enable(self.storyPanel.canUndo()) undoItem.SetText('Undo ' + self.storyPanel.undoAction() + '\tCtrl-Z' if self.storyPanel.canUndo() else "Can't Undo\tCtrl-Z") redoItem = self.menus.FindItemById(wx.ID_REDO) redoItem .Enable(self.storyPanel.canRedo()) redoItem .SetText('Redo ' + self.storyPanel.redoAction() + '\tCtrl-Y' if self.storyPanel.canRedo() else "Can't Redo\tCtrl-Y") for item in wx.ID_CUT, wx.ID_COPY, wx.ID_DELETE: self.menus.FindItemById(item).Enable(selections > 0) self.menus.FindItemById(StoryFrame.EDIT_FIND_NEXT).Enable(self.storyPanel.lastSearchRegexp is not None) # View menu self.menus.FindItemById(StoryFrame.VIEW_TOOLBAR).Check(self.showToolbar) self.menus.FindItemById(StoryFrame.VIEW_SNAP).Check(self.storyPanel.snapping) # Story menu, Build menu editItem = self.menus.FindItemById(wx.ID_EDIT) testItem = self.menus.FindItemById(StoryFrame.BUILD_TEST_HERE) if selections == 1: widget = self.storyPanel.selectedWidget() editItem.SetItemLabel("Edit \"" + widget.passage.title + "\"") editItem.Enable(True) # Only allow test plays from story passages testItem.SetItemLabel("Test Play From \"" + widget.passage.title + "\"" if widget.passage.isStoryPassage() else "Test Play From Here") testItem.Enable(widget.passage.isStoryPassage()) else: editItem.SetItemLabel("&Edit Passage") editItem.Enable(False) testItem.SetItemLabel("Test Play From Here") testItem.Enable(False) self.menus.FindItemById(StoryFrame.STORY_EDIT_FULLSCREEN).Enable(selections == 1) self.menus.FindItemById(StoryFrame.BUILD_REBUILD).Enable(self.buildDestination != '') self.menus.FindItemById(StoryFrame.BUILD_VIEW_LAST).Enable(self.buildDestination != '') hasStoryIncludes = self.buildDestination != '' and 'StoryIncludes' in self.storyPanel.widgetDict self.autobuildmenuitem.Enable(hasStoryIncludes) self.menus.FindItemById(StoryFrame.REFRESH_INCLUDES_LINKS).Enable(hasStoryIncludes) # Story format submenu for key in self.storyFormats: self.menus.FindItemById(key).Check(self.target == self.storyFormats[key].id) 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(), 'metadata': self.metadata, } 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(), 'metadata': self.metadata, } def __repr__(self): return "" def getHeader(self): """Returns the current selected target header for this Story Frame.""" return self.header # 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, STORY_NEW_SCRIPT, STORY_NEW_STYLESHEET, STORY_NEW_ANNOTATION, STORY_EDIT_FULLSCREEN, STORY_STATS, STORY_METADATA, \ STORY_IMPORT_IMAGE, STORY_IMPORT_IMAGE_URL, STORY_IMPORT_FONT, STORY_FORMAT_HELP, STORYSETTINGS_START, STORYSETTINGS_TITLE, STORYSETTINGS_SUBTITLE, STORYSETTINGS_AUTHOR, \ STORYSETTINGS_MENU, STORYSETTINGS_SETTINGS, STORYSETTINGS_INCLUDES, STORYSETTINGS_INIT, STORYSETTINGS_HELP, REFRESH_INCLUDES_LINKS] = range(401, 422) STORY_FORMAT_BASE = 501 [BUILD_VERIFY, BUILD_TEST, BUILD_TEST_HERE, BUILD_BUILD, BUILD_REBUILD, BUILD_VIEW_LAST, BUILD_AUTO_BUILD] = range( 601, 608) [HELP_MANUAL, HELP_GROUP, HELP_GITHUB, HELP_FORUM] = range(701, 705) # 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' class ClipboardMonitor(wx.Timer): """ Monitors the clipboard and notifies a callback when the format of the contents changes from or to Twine passage data. """ def __init__(self, callback): wx.Timer.__init__(self) self.callback = callback self.dataFormat = wx.CustomDataFormat(StoryPanel.CLIPBOARD_FORMAT) self.state = None def Notify(self, *args, **kwargs): if wx.TheClipboard.Open(): newState = wx.TheClipboard.IsSupported(self.dataFormat) wx.TheClipboard.Close() if newState != self.state: self.state = newState self.callback(newState)