diff --git a/app.py b/app.py index 71516fc..04fcf25 100755 --- a/app.py +++ b/app.py @@ -1,7 +1,9 @@ #!/usr/bin/env python2 -import sys, os, locale, re, pickle, wx, platform, traceback +import sys, os, locale, re, simplejson, pickle, jsonpickle, wx, platform, traceback, time import metrics +from tiddlywiki import TiddlyWiki +from tiddlywiki import Tiddler from header import Header from storyframe import StoryFrame from prefframe import PreferenceFrame @@ -33,12 +35,13 @@ def __init__(self, redirect = False): self.icon = wx.EmptyIcon() + jsonpickle.set_encoder_options('simplejson', sort_keys=True, indent=4) + try: self.icon = wx.Icon(self.iconsPath + 'app.ico', wx.BITMAP_TYPE_ICO) except: pass - # restore save location try: @@ -74,9 +77,10 @@ def removeStory(self, story, byMenu = False): except ValueError: pass - def openDialog(self, event = None): + def openDialog(self, event=None): """Opens a story file of the user's choice.""" - dialog = wx.FileDialog(None, 'Open Story', os.getcwd(), "", "Twine Story (*.tws)|*.tws", \ + fileExtension = "Twine Story (*.tws)|*.tws|JSON Twine Story (*.twjs)|*.twjs" + dialog = wx.FileDialog(None, 'Open Story', os.getcwd(), "", fileExtension, \ wx.FD_OPEN | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: @@ -102,14 +106,27 @@ def MacOpenFile(self, path): def open(self, path): """Opens a specific story file.""" try: + usejson = True + if path[len(path)-3:] == "tws": + usejson = False + openedFile = open(path, 'r') - newStory = StoryFrame(None, app = self, state = pickle.load(openedFile)) + fileData = openedFile.read() + openedFile.close() + + if usejson: + state = jsonpickle.decode(fileData) + else: + state = pickle.loads(fileData) + + newStory = StoryFrame(None, app = self, state = state) + newStory.saveDestination = path + self.stories.append(newStory) newStory.Show(True) self.addRecentFile(path) self.config.Write('LastFile', path) - openedFile.close() # weird special case: # if we only had one story opened before diff --git a/storyframe.py b/storyframe.py index e0afa17..420f6b6 100644 --- a/storyframe.py +++ b/storyframe.py @@ -1,6 +1,8 @@ -import sys, re, os, urllib, urlparse, pickle, wx, codecs, tempfile, images, version +import sys, re, os, urllib, urlparse, simplejson, pickle, jsonpickle, wx, codecs, tempfile, images, version from wx.lib import imagebrowser from tiddlywiki import TiddlyWiki +from tiddlywiki import Tiddler +import time from storypanel import StoryPanel from passagewidget import PassageWidget from statisticsdialog import StatisticsDialog @@ -8,7 +10,6 @@ from storymetadataframe import StoryMetadataFrame from utils import isURL - class StoryFrame(wx.Frame): """ A StoryFrame displays an entire story. Its main feature is an @@ -90,6 +91,9 @@ def __init__(self, parent, app, state=None, refreshIncludes=True): 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(StoryFrame.SAVEAS_JSON, 'S&ave Story As (JSON)...') + self.Bind(wx.EVT_MENU, self.saveAsJSON, id=StoryFrame.SAVEAS_JSON) + fileMenu.Append(wx.ID_REVERT_TO_SAVED, '&Revert to Saved') self.Bind(wx.EVT_MENU, self.revert, id=wx.ID_REVERT_TO_SAVED) @@ -486,7 +490,7 @@ def checkCloseDo(self, event, byMenu): elif result == wx.ID_NO: self.dirty = False else: - self.save(None) + self.save() if self.dirty: event.Veto() return @@ -509,10 +513,24 @@ def checkCloseDo(self, event, byMenu): event.Skip() self.Destroy() - def saveAs(self, event=None): + def exportJSON(self, event=None): + self.saveAsMain(True,event) + + def saveAs(self, event=None): + self.saveAsMain(False,event) + + def saveAsJSON(self, event=None): + self.saveAsMain(True,event) + + def saveAsMain(self, usejson=False, event=None): """Asks the user to choose a file to save state to, then passes off control to save().""" + + extensionStr = "Twine Story (*.tws)|*.tws|Twine Story without private content [copy] (*.tws)|*.tws" + if usejson: + extensionStr = "JSON Twine Story (*.twjs)|*.twjs|JSON Twine Story without private content [copy] (*.twjs)|*.twjs" + dialog = wx.FileDialog(self, 'Save Story As', os.getcwd(), "", \ - "Twine Story (*.tws)|*.tws|Twine Story without private content [copy] (*.tws)|*.tws", \ + extensionStr, \ wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT | wx.FD_CHANGE_DIR) if dialog.ShowModal() == wx.ID_OK: @@ -520,13 +538,20 @@ def saveAs(self, event=None): self.saveDestination = dialog.GetPath() self.app.config.Write('savePath', os.getcwd()) self.app.addRecentFile(self.saveDestination) - self.save(None) + self.save(event) elif dialog.GetFilterIndex() == 1: npsavedestination = dialog.GetPath() try: + if usejson: + fileData = jsonpickle.encode(self.serialize_noprivate(npsavedestination)) + fileData = self.storyPanel.stripTimeFields( fileData ) + else: + fileData = pickle.dumps(self.serialize_noprivate(npsavedestination)) + dest = open(npsavedestination, 'wb') - pickle.dump(self.serialize_noprivate(npsavedestination), dest) + dest.write(fileData) dest.close() + self.app.addRecentFile(npsavedestination) except: self.app.displayError('saving your story') @@ -819,13 +844,23 @@ def createInfoPassage(self, event): def save(self, event=None): if self.saveDestination == '': - self.saveAs() + self.saveAsMain(False,event) return - try: - dest = open(self.saveDestination, 'wb') - pickle.dump(self.serialize(), dest) + usejson = True + if self.saveDestination[len(self.saveDestination)-3:] == "tws": + usejson = False + + if usejson: + fileData = jsonpickle.encode(self.serialize()) + fileData = self.storyPanel.stripTimeFields( fileData ) + else: + fileData = pickle.dumps(self.serialize()) + + dest = open(self.saveDestination, 'wb') + dest.write(fileData) dest.close() + self.setDirty(False) self.app.config.Write('LastFile', self.saveDestination) except: @@ -945,7 +980,7 @@ def getLocalDir(self): dir = os.getcwd() return dir - def readIncludes(self, lines, callback, silent=False): + def readIncludes(self, lines, callback, usejson=True, silent=False): """ Examines all of the source files included via StoryIncludes, and performs a callback on each passage found. @@ -970,9 +1005,14 @@ def readIncludes(self, lines, callback, silent=False): openedFile = open(os.path.join(twinedocdir, line), 'r') if extension == '.tws': - s = StoryFrame(None, app=self.app, state=pickle.load(openedFile), refreshIncludes=False) + fileData = openedFile.read() openedFile.close() + if usejson: + s = StoryFrame(None, app=self.app, state=jsonpickle.decode(fileData), refreshIncludes=False) + else: + s = StoryFrame(None, app=self.app, state=pickle.loads(fileData), refreshIncludes=False) + for widget in s.storyPanel.widgetDict.itervalues(): if excludetags.isdisjoint(widget.passage.tags): callback(widget.passage) @@ -1277,6 +1317,8 @@ def getHeader(self): FILE_EXPORT_SOURCE = 103 FILE_IMPORT_HTML = 104 + SAVEAS_JSON = 1105 + EDIT_FIND_NEXT = 201 VIEW_SNAP = 301 diff --git a/storypanel.py b/storypanel.py index 8502cc1..f991f87 100644 --- a/storypanel.py +++ b/storypanel.py @@ -3,8 +3,21 @@ import sys, wx, re, pickle import geometry from tiddlywiki import TiddlyWiki +import time from passagewidget import PassageWidget +# r + s1 + r + s2 + r + ... + sn + r +def unfoldPattern(s,r): + pat = '' + e = '[]{}.?*^+-' + for c in s: + pat += r + if c in e: + pat += '\\' + pat += c + pat += r + return pat + class StoryPanel(wx.ScrolledWindow): """ A StoryPanel is a container for PassageWidgets. It translates @@ -45,7 +58,8 @@ def __init__(self, parent, app, id = wx.ID_ANY, state = None): self.tooltipplace = None self.tooltipobj = None self.textDragSource = None - + #self.timeFields = re.compile(',?[^\{\}\[\],]*"(created|modified)": (' + unfoldPattern('{,[{},{[{[,,,,,,,,]},{}]}]}', '[^\{\}\[\],]*') + '|\{[^\{\}]+\})') + self.timeFields = re.compile('[^\{\}\[\],]*"(created|modified)": (' + unfoldPattern('{[{},{[{[,,,,,,,,]},{}]}],}', '[^\{\}\[\],]*') + '|\{[^\{\}]+\}),?') if state: self.scale = state['scale'] for widget in state['widgets']: @@ -83,6 +97,9 @@ def __init__(self, parent, app, id = wx.ID_ANY, state = None): self.Bind(wx.EVT_LEAVE_WINDOW, self.handleHoverStop) self.Bind(wx.EVT_MOTION, self.handleHover) + def stripTimeFields( self, s ): + return self.timeFields.sub('',s) + def newWidget(self, title = None, text = '', tags = (), pos = None, quietly = False, logicals = False): """Adds a new widget to the container.""" @@ -154,6 +171,7 @@ def copyWidgets(self): if widget.selected: data.append(widget.serialize()) clipData = wx.CustomDataObject(wx.CustomDataFormat(StoryPanel.CLIPBOARD_FORMAT)) + clipData.SetData(pickle.dumps(data, 1)) if wx.TheClipboard.Open(): @@ -176,7 +194,8 @@ def pasteWidgets(self, pos = (0,0), logicals = False): wx.TheClipboard.Close() if gotData: - data = pickle.loads(clipData.GetData()) + + data = pickle.loads(clipData.GetData()) self.eachWidget(lambda w: w.setSelected(False, False)) diff --git a/tiddlywiki.py b/tiddlywiki.py index c38cd83..6cf88d3 100644 --- a/tiddlywiki.py +++ b/tiddlywiki.py @@ -324,7 +324,7 @@ class Tiddler: # pylint: disable=old-style-class Note: Converting this to a new-style class breaks pickling of new TWS files on old Twine releases. """ - def __init__(self, source, type = 'twee', obfuscatekey = ""): + def __init__(self, source = "", type = 'twee', obfuscatekey = ""): # cache of passage names linked from this one self.links = [] self.displays = [] @@ -345,9 +345,12 @@ def __getstate__(self): 'modified': now, 'title': self.title, 'tags': self.tags, - 'text': self.text, + 'text': self.text } + def __setstate__(self,d): + self.__dict__ = d + def __repr__(self): return ""