diff --git a/package.json b/package.json index 4e366ab87..eefcc0d94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "issie", - "version": "1.1.0", + "version": "1.1.1", "description": "Schematic editor and Simulator", "homepage": "https://github.com/tomcl/issie", "bugs": { @@ -39,7 +39,7 @@ "title": true }, "build": { - "appId": "ISSIE.110", + "appId": "ISSIE.111", "asar": true, "win": { "target": "zip" diff --git a/src/Renderer/Interface/FilesIO.fs b/src/Renderer/Interface/FilesIO.fs index 061660d09..925f6f116 100644 --- a/src/Renderer/Interface/FilesIO.fs +++ b/src/Renderer/Interface/FilesIO.fs @@ -16,10 +16,33 @@ open Node open EEExtensions -let private fileExistsWithExtn extn folderPath baseName = + +let fileExistsWithExtn extn folderPath baseName = let path = path.join [| folderPath; baseName + extn |] fs.existsSync (U2.Case1 path) +let readFilesFromDirectory (path:string) : string list = + if fs.existsSync (U2.Case1 path) then + fs.readdirSync(U2.Case1 path) + |> Seq.toList + else + [] + +/// returns the sequence number and name of the most recent (highest sequence number) backup file +let latestBackupFileData (path:string) (baseName: string) = + readFilesFromDirectory path + |> List.filter (fun fn -> String.startsWith (baseName + "-") fn) + |> List.map (fun fn -> + String.splitString [|"-"|] fn + |> Array.tryItem 1 + |> Option.bind (String.tryParseWith System.Int32.TryParse) + |> fun n -> n,fn) + |> List.sortDescending + |> List.tryHead + |> Option.bind (function | None,_ -> None | Some n, fn -> Some(n, fn)) + +/// read canvas state from file found on filePath (which includes .dgm suffix etc). +/// return Error if file does not exist or cannot be parsed. let private tryLoadStateFromPath (filePath: string) = if not (fs.existsSync (U2.Case1 filePath)) then Error <| sprintf "Can't read file from %s because it does not seem to exist!" filePath @@ -34,6 +57,12 @@ let private tryLoadStateFromPath (filePath: string) = let pathJoin args = path.join args let baseName filePath = path.basename filePath + +let baseNameWithoutExtension filePath = + baseName filePath + |> String.splitString [|"."|] + |> function | [|name ; extn|] -> name | arr -> arr.[0] + let dirName filePath = path.dirname filePath let ensureDirectory dPath = if (not <| fs.existsSync (U2.Case1 dPath)) then @@ -238,8 +267,9 @@ let makeLoadedComponentFromCanvasData canvas filePath timeStamp waveInfo = } - -let private tryLoadComponentFromPath filePath : Result = +/// Make a loadedComponent from the file read from filePath. +/// Return the component, or an Error string. +let tryLoadComponentFromPath filePath : Result = match tryLoadStateFromPath filePath with | Result.Error msg -> Error <| sprintf "Can't load component %s because of Error: %s" (getBaseNameNoExtension filePath) msg | Ok state -> @@ -282,3 +312,6 @@ let loadAllComponentFiles (folderPath:string) = | Error msg, _ -> Error msg ) |> tryFindError + + + diff --git a/src/Renderer/Interface/Version.fs b/src/Renderer/Interface/Version.fs index b12ac3ac7..6653939e4 100644 --- a/src/Renderer/Interface/Version.fs +++ b/src/Renderer/Interface/Version.fs @@ -1,5 +1,5 @@ module Version -let VERSION = [ 1 ; 1 ; 0] +let VERSION = [ 1 ; 1 ; 1] // The first 12 white-space separated words in this file must be in the above format - note that spaces are required. // This works as valid F# data for displaying the code version and can also be read programmatically from the master branch github file diff --git a/src/Renderer/UI/FileMenuView.fs b/src/Renderer/UI/FileMenuView.fs index 71b8b1a20..de38ac503 100644 --- a/src/Renderer/UI/FileMenuView.fs +++ b/src/Renderer/UI/FileMenuView.fs @@ -51,6 +51,72 @@ let releaseFileActivityImplementation a = +/// Works out number of components and connections changed between two LoadedComponent circuits +/// a new ID => a change even if the circuit topology is identical. Layout differences do not +/// mean changes, as is implemented in the reduce functions which remove layout. +let quantifyChanges (ldc1:LoadedComponent) (ldc2:LoadedComponent) = + let comps1,conns1 = ldc1.CanvasState + let comps2,conns2 = ldc2.CanvasState + let reduceComp comp1 = + {comp1 with X=0;Y=0} + let reduceConn conn1 = + {conn1 with Vertices = []} + /// Counts the number of unequal items in the two lists. + /// Determine equality from whether reduce applied to each item is equal + let unmatched reduce lst1 lst2 = + let mapToSet = List.map reduce >> Set + let rL1, rL2 = mapToSet lst1, mapToSet lst2 + Set.union (Set.difference rL1 rL2) (Set.difference rL2 rL1) + |> Set.count + unmatched reduceComp comps1 comps2, unmatched reduceConn conns1 conns2 + +//------------------------------------------Backup facility-------------------------------------------// + +let writeComponentToFile comp = + let data = stateToJsonString (comp.CanvasState,comp.WaveInfo) + writeFile comp.FilePath data + +/// return an option containing sequence data and file path of the latest +/// backup file for given component, if it exists. +let readLastBackup comp = + let path = pathWithoutExtension comp.FilePath + let baseN = baseName path + let backupDir = pathJoin [| dirName path ; "backup" |] + latestBackupFileData backupDir baseN + + + +/// Write comp to a backup file unless the latest backup canvas is within numChanges distance from +/// the comp canvas. +let writeComponentToBackupFile numChanges comp = + let nSeq, backFilePath = + match readLastBackup comp with + | Some( n, fp) -> n+1,fp + | None -> 0, "" + let wantToWrite = + if backFilePath = "" then + true + else + match tryLoadComponentFromPath backFilePath with + | Ok comp' -> + let nComps,nConns = quantifyChanges comp' comp + nComps + nConns >= numChanges + | _ -> true + if wantToWrite then + let path = pathWithoutExtension comp.FilePath + let baseN = baseName path + let timestamp = System.DateTime.Now + let ds = EEExtensions.String.replaceChar '/' '-' (timestamp.ToShortDateString()) + let suffix = EEExtensions.String.replaceChar ' ' '-' (sprintf "%s-%02dh-%02dm" ds timestamp.Hour timestamp.Minute) + ensureDirectory <| pathJoin [| dirName path ; "backup" |] + let backupDir = pathJoin [| dirName path ; "backup" |] + let backupPath = pathJoin [| dirName path ; "backup" ; sprintf "%s-%03d-%s.dgm" baseN nSeq suffix |] + printfn "Writing backup file to %s" backupPath + {comp with + TimeStamp = timestamp + FilePath = backupPath} + |> writeComponentToFile + /// returns a WaveSimModel option if a file is loaded, otherwise None let currWaveSimModel (model: Model) = match getCurrFile model with @@ -130,6 +196,10 @@ let updateLoadedComponents name (setFun: LoadedComponent -> LoadedComponent) (lc printf "In updateLoadedcomponents can't find name='%s' in components:%A" name lcLst lcLst | Some n -> + let newLc = setFun lcLst.[n] + let nComps,nConns = quantifyChanges newLc lcLst.[n] + if nComps + nConns > 0 then + writeComponentToBackupFile 0 newLc List.mapi (fun i x -> if i = n then setFun x else x) lcLst /// return current project with current sheet updated from canvas if needed @@ -198,26 +268,32 @@ let saveOpenFileAction isAuto model = Some (makeLoadedComponentFromCanvasData state origLdComp.FilePath DateTime.Now savedWaveSim, reducedState)) + // save current open file, updating model etc, and returning the loaded component and the saved (unreduced) canvas state let saveOpenFileActionWithModelUpdate (model: Model) (dispatch: Msg -> Unit) = - let opt = saveOpenFileAction false model - let ldcOpt = Option.map fst opt - let reducedState = Option.map snd opt |> Option.defaultValue ([],[]) - match model.CurrentProj with - | None -> failwithf "What? Should never be able to save sheet when project=None" - | Some p -> - // update loaded components for saved file - updateLdCompsWithCompOpt ldcOpt p.LoadedComponents - |> (fun lc -> {p with LoadedComponents=lc}) - |> SetProject - |> dispatch - // update Autosave info - SetLastSavedCanvas (p.OpenFileName,reducedState) - |> dispatch - SetHasUnsavedChanges false - |> JSDiagramMsg - |> dispatch - opt + if requestFileActivity "save" dispatch then + let opt = saveOpenFileAction false model + let ldcOpt = Option.map fst opt + let reducedState = Option.map snd opt |> Option.defaultValue ([],[]) + match model.CurrentProj with + | None -> failwithf "What? Should never be able to save sheet when project=None" + | Some p -> + // update loaded components for saved file + updateLdCompsWithCompOpt ldcOpt p.LoadedComponents + |> (fun lc -> {p with LoadedComponents=lc}) + |> SetProject + |> dispatch + // update Autosave info + SetLastSavedCanvas (p.OpenFileName,reducedState) + |> dispatch + SetHasUnsavedChanges false + |> JSDiagramMsg + |> dispatch + releaseFileActivity "save" dispatch + opt + else + None + let private getFileInProject name project = project.LoadedComponents |> List.tryFind (fun comp -> comp.Name = name) @@ -299,9 +375,10 @@ let private openFileInProject' saveCurrent name project (model:Model) dispatch = match updateProjectFromCanvas model with | None -> failwithf "What? current project cannot be None at this point in openFileInProject" | Some p -> + let updatedModel = {model with CurrentProj = Some p} let ldcs = if saveCurrent then - let opt = saveOpenFileAction false model + let opt = saveOpenFileAction false updatedModel let ldcOpt = Option.map fst opt let ldComps = updateLdCompsWithCompOpt ldcOpt project.LoadedComponents let reducedState = Option.map snd opt |> Option.defaultValue ([],[]) @@ -382,7 +459,8 @@ let renameSheet oldName newName (model:Model) dispatch = releaseFileActivity "renameSheet" dispatch failwithf "What? current project cannot be None at this point in renamesheet" | Some p -> - let opt = saveOpenFileAction false model + let updatedModel = {model with CurrentProj = Some p} + let opt = saveOpenFileAction false updatedModel let ldcOpt = Option.map fst opt let ldComps = updateLdCompsWithCompOpt ldcOpt p.LoadedComponents let reducedState = Option.map snd opt |> Option.defaultValue ([],[]) @@ -567,24 +645,7 @@ let private newProject model dispatch _ = -/// Works out number of components and connections changed between two LoadedComponent circuits -/// a new ID => a change even if the circuit topology is identical. Layout differences do not -/// mean changes, as is implemented in the reduce functions which remove layout. -let quantifyChanges (ldc1:LoadedComponent) (ldc2:LoadedComponent) = - let comps1,conns1 = ldc1.CanvasState - let comps2,conns2 = ldc2.CanvasState - let reduceComp comp1 = - {comp1 with X=0;Y=0} - let reduceConn conn1 = - {conn1 with Vertices = []} - /// Counts the number of unequal items in the two lists. - /// Determine equality from whether reduce applied to each item is equal - let unmatched reduce lst1 lst2 = - let mapToSet = List.map reduce >> Set - let rL1, rL2 = mapToSet lst1, mapToSet lst2 - Set.union (Set.difference rL1 rL2) (Set.difference rL2 rL1) - |> Set.count - unmatched reduceComp comps1 comps2, unmatched reduceConn conns1 conns2 + @@ -606,23 +667,11 @@ let rec resolveComponentOpenPopup | Resolve (ldComp,autoComp) :: rLst -> // ldComp, autocomp are from attemps to load saved file and its autosave version. let compChanges, connChanges = quantifyChanges ldComp autoComp - let writeComponentToFile comp = - let data = stateToJsonString (comp.CanvasState,comp.WaveInfo) - writeFile comp.FilePath data let buttonAction autoSave _ = let comp = {(if autoSave then autoComp else ldComp) with TimeStamp = DateTime.Now} writeComponentToFile comp - if compChanges + connChanges >= 3 then - let backupComp = - let comp = if autoSave then ldComp else autoComp - let path = pathWithoutExtension comp.FilePath + ".dgm" - ensureDirectory <| pathJoin [| dirName path ; "backup" |] - let backupPath = pathJoin [| dirName path ; "backup" ; baseName path |] - printfn "Writing backup file to %s" backupPath - {comp with - TimeStamp = DateTime.Now - FilePath = backupPath} - writeComponentToFile backupComp + if compChanges + connChanges > 0 then + writeComponentToBackupFile 0 comp resolveComponentOpenPopup pPath (comp :: components) rLst model dispatch // special case when autosave data is most recent let title = "Warning!"