1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333 |
- 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 "<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)
|