Skip to content
Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1346 lines (1057 sloc) 55.8 KB
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)[:2]
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, .woff2, .svg)|*.ttf;*.otf;*.woff;*.woff2;*.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, ext = os.path.splitext(os.path.basename(file))
return (images.addURIPrefix(file64, ext[1:]), title, ext[1:])
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, ext = self.openFileAsBase64(file)
# Determine the font format (correction required for TTF/OTF)
if ext == '':
fontformat = ""
else:
if ext == 'ttf':
ext = "truetype"
elif ext == 'otf':
ext = "opentype"
fontformat = ' format("' + ext + '")'
title2 = self.newTitle(title)
# Wrap in CSS @font-face declaration
text = \
'''@font-face {
\tfont-family: "''' + title + '''";
\tsrc: url("''' + text + '''")''' + fontformat + ''';
}
font[face="''' + title + '''"] {
\tfont-family: "''' + title + '''";
}'''
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='Start'):
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):
if target not in self.app.headers:
self.app.displayError("opening the last edited story: the story format '" + target + "' isn't available.\n"
+ "Please select another format from the Story Format submenu", False)
return self.setTarget(self.app.headers.iterkeys().next())
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 "<StoryFrame '" + self.saveDestination + "'>"
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)
You can’t perform that action at this time.