diff --git a/src/Grafoscopio/GrafoscopioNode.class.st b/src/Grafoscopio/GrafoscopioNode.class.st index 1c6c178..28ef407 100644 --- a/src/Grafoscopio/GrafoscopioNode.class.st +++ b/src/Grafoscopio/GrafoscopioNode.class.st @@ -13,8 +13,13 @@ Class { #superclass : #Object, #instVars : [ 'header', + 'created', + 'edited', 'headers', 'key', + 'id', + 'selected', + 'expanded', 'icon', 'body', 'tags', @@ -102,7 +107,10 @@ GrafoscopioNode >> addNode: aNode [ GrafoscopioNode >> addNodeAfterMe [ "Adds a generic node after the given node so they become slibings of the same parent" | genericNode | - genericNode := self class new header: 'newNode'; body: ''. + genericNode := self class new + created: DateAndTime now printString; + header: 'newNode'; + body: ''. self parent children add: genericNode after: self. genericNode parent: self parent. genericNode level: self level. @@ -182,6 +190,17 @@ GrafoscopioNode >> asSton [ "Exports current tree as STON format" | stonOutput | + stonOutput := '' writeStream. + stonOutput nextPutAll: (STON toStringPretty: self "flatten"). + ^stonOutput contents + +] + +{ #category : #exporting } +GrafoscopioNode >> asStonFromRoot [ + "Exports current tree as STON format" + | stonOutput | + stonOutput := '' writeStream. self flatten. stonOutput nextPutAll: (STON toStringPretty: self children). @@ -198,24 +217,29 @@ GrafoscopioNode >> asText [ GrafoscopioNode >> becomeDefaultTestTree [ | node1 node2 node3 node4 | self - level: 0; + created: DateAndTime now printString; + level: 0; header: 'Arbol principal'. node1 := self class new + created: DateAndTime now printString; header: 'Markup'; body: 'I am just a node with markup'; tagAs: 'text'; links: 'temp.md'; level: 1. node2 := self class new + created: DateAndTime now printString; header: '%output Code'; tagAs: 'código'; body: '(ConfigurationOfGrafoscopio>>#version14:) sourceCode'. node3 := self class new + created: DateAndTime now printString; header: '%invisible'; tagAs: 'text'; body: 'Just testing'. node1 addNode: node3. node4 := self class new + created: DateAndTime now printString; header: 'Something'; tagAs: 'text'; body: '

else

'. @@ -231,10 +255,12 @@ GrafoscopioNode >> becomeDefaultTree [ | node1 | self class new. self + created: DateAndTime now printString; level: 0; header: 'Arbol principal'; tagAs: 'código'. node1 := self class new + created: DateAndTime now printString; header: 'Node 1'; body: ''; tagAs: 'text'. @@ -252,8 +278,6 @@ GrafoscopioNode >> body [ { #category : #accessing } GrafoscopioNode >> body: anObject [ - "Sets the receivers body to the given object" - body := anObject ] @@ -278,9 +302,26 @@ GrafoscopioNode >> bodyAsMarkdownInto: aStream [ { #category : #operation } GrafoscopioNode >> checksum [ + "I return the SHA1SUM of the current node. + I'm used to test changes on the node contents, without including changes in the children." + | nodeCopy | + nodeCopy := self surfaceCopy. + ^ self checksumFor: nodeCopy asSton. +] + +{ #category : #utility } +GrafoscopioNode >> checksumFor: aText [ "I return the SHA1SUM of the current tree. I'm used to test changes on the contents and for traceability of how the document tree is converted to other formats, as markdown." - ^ (SHA1 new hashMessage: self root flatten asSton) hex + ^ (SHA1 new hashMessage: aText) hex +] + +{ #category : #operation } +GrafoscopioNode >> checksumForRootSubtree [ + "I return the SHA1SUM of the current tree. I'm used to test changes on the contents + and for traceability of how the document tree is converted to other formats, as markdown." + ^ self checksumFor: self root flatten asStonFromRoot. + "^ (SHA1 new hashMessage: self root flatten asStonFromRoot) hex" ] { #category : #accessing } @@ -302,7 +343,7 @@ GrafoscopioNode >> children: aCollection [ GrafoscopioNode >> content [ "Returns the receivers body" - ^ body + ^ self body ] @@ -320,6 +361,19 @@ GrafoscopioNode >> copyToClipboard [ ] +{ #category : #accessing } +GrafoscopioNode >> created [ + + ^ created +] + +{ #category : #accessing } +GrafoscopioNode >> created: aTimestamp [ + "I tell when this object was created" + + created := aTimestamp +] + { #category : #operation } GrafoscopioNode >> currentLink [ "TODO: This method should not only select sanitized links, but also provide ways to detect wich link @@ -350,6 +404,34 @@ GrafoscopioNode >> demote [ ] +{ #category : #'as yet unclassified' } +GrafoscopioNode >> detectSelectionIndex [ + "I tell which is the index of the current selected node or return the first childre + (indexed at 1) if is not found." + + | root | + root := self root. + root preorderTraversal allButFirst doWithIndex: [ :currentNode :index | + currentNode isSelected ifTrue: [ ^ index ] ]. + ^ 1. +] + +{ #category : #accessing } +GrafoscopioNode >> edited [ + ^ edited +] + +{ #category : #accessing } +GrafoscopioNode >> edited: aTimestamp [ + "I store the last time when a node was edited. + Because nodes in the notebook have a autosave feature, I'm updated automatically when nodes are + edited from the GUI. + + If I'm in the notebook root (i.e. node's level equals 0) I should store the last time the notebook + was saved on the hard drive." + edited := aTimestamp +] + { #category : #'custom markup' } GrafoscopioNode >> embedAll [ "This is just a previous part of the messy markDownContent. The %embed-all keyword should be revaluated. @@ -384,6 +466,13 @@ GrafoscopioNode >> embeddedNodes [ ^ self children select: [:each | each headerStartsWith: '%embed'] ] +{ #category : #accessing } +GrafoscopioNode >> expanded: aBoolean [ + "I tell if the node is expanded from the UI, showing my children. + Several nodes can be expanded in a single document." + selected := aBoolean +] + { #category : #exporting } GrafoscopioNode >> exportCodeBlockTo: aStream [ "I convert the content of a node taged as 'código' (code) as pandoc markdown and put it @@ -564,6 +653,18 @@ GrafoscopioNode >> icon: aSymbol [ icon := aSymbol ] +{ #category : #accessing } +GrafoscopioNode >> id [ + ^id +] + +{ #category : #accessing } +GrafoscopioNode >> id: aChecksum [ + "I'm a unique identifier that changes when node content changes (i.e. header, body, links)." + + id := aChecksum +] + { #category : #importing } GrafoscopioNode >> importHtmlLink [ "I take the last link and import its contents in node body. " @@ -607,6 +708,21 @@ GrafoscopioNode >> isEmpty [ body ifNil: [ ^ true ] ifNotNil: [ ^ false ] ] +{ #category : #operation } +GrafoscopioNode >> isSavedAfterLastEdition [ + | root | + root := self root. + root edited ifNil: [ ^ false ]. + ^ self unsavedNodes isEmpty. + "self unsavedNodes isEmpty ifFalse: [ ^ self unsavedNodes inspect ]" +] + +{ #category : #testing } +GrafoscopioNode >> isSelected [ + self selected ifNil: [ ^ false ]. + ^ self selected. +] + { #category : #operation } GrafoscopioNode >> isTaggedAs: aString [ self tags ifEmpty: [ self tagAs: 'text' ]. @@ -688,12 +804,12 @@ GrafoscopioNode >> links: anObject [ ] { #category : #operation } -GrafoscopioNode >> linksToMarkdownFile [ +GrafoscopioNode >> linksToMarkupFile [ "I detect if the links contains any reference to a file ending in '.md' or '.markdown'" self links ifNotNil: [ self links - detect: [:l | (l endsWith: '.md') or: [ l endsWith: '.markdown']] + detect: [:l | (l endsWithAnyOf: #('.md' '.markdown' '.md.html'))] ifFound: [ ^ true ] ifNone: [^ false]]. ^ false @@ -901,6 +1017,21 @@ GrafoscopioNode >> preorderTraversal [ ^ nodesInPreorder. ] +{ #category : #'as yet unclassified' } +GrafoscopioNode >> preorderTraversalIndex [ + "I tell which place I occupy in the tree children (without counting the root)." + + | root | + root := self root. + root preorderTraversalRootChildren doWithIndex: [ :currentNode :index | + currentNode = self ifTrue: [^ index] ]. +] + +{ #category : #'as yet unclassified' } +GrafoscopioNode >> preorderTraversalRootChildren [ + ^ self root preorderTraversal allButFirst +] + { #category : #movement } GrafoscopioNode >> promote [ "Moves the current node up in the hierachy, making it a slibing of its current parent" @@ -991,8 +1122,20 @@ GrafoscopioNode >> saveContent: anObject [ ] { #category : #operation } -GrafoscopioNode >> selectMarkdownSubtreesToExport [ - ^ (self root preorderTraversal) select: [ :each | each linksToMarkdownFile ]. +GrafoscopioNode >> selectMarkupSubtreesToExport [ + ^ (self root preorderTraversal) select: [ :each | each linksToMarkupFile ]. +] + +{ #category : #accessing } +GrafoscopioNode >> selected [ + ^ selected +] + +{ #category : #accessing } +GrafoscopioNode >> selected: aBoolean [ + "I tell if the node is selected from the UI. + Once other node is selected my value becomes false." + selected := aBoolean ] { #category : #accessing } @@ -1024,10 +1167,13 @@ GrafoscopioNode >> surfaceCopy [ to the rest of the container tree, which could end in copying the whole tree." | newNode | newNode := self class new. - ^ newNode + newNode header: self header; body: self body; - tags: self tags. + tags: self tags; + level: self level. + self links ifNotEmpty: [ newNode links addAll: self links ]. + ^ newNode. ] @@ -1083,6 +1229,36 @@ GrafoscopioNode >> toggleCodeText [ ifTrue: [ ^ self tags replaceAll: 'código' with: 'text' ]. ] +{ #category : #accessing } +GrafoscopioNode >> toggleSelected [ + "I made the receiver the current selected node and deselect all other nodes." + + | root previousSelection | + self isSelected ifTrue: [ ^ self ]. + root := self root. + previousSelection := self preorderTraversalRootChildren at: (self detectSelectionIndex). + previousSelection selected: false. + self selected: true. + ^ self. +] + +{ #category : #operation } +GrafoscopioNode >> unsavedNodes [ + "I collect all nodes that have changed after the last saving" + | lastSavedOn root unsavedNodes | + root := self root. + lastSavedOn := root edited asDateAndTime. + unsavedNodes := root preorderTraversal select: [ :currentNode | + currentNode edited isNotNil and: [currentNode edited asDateAndTime > lastSavedOn] ]. + ^ unsavedNodes. + +] + +{ #category : #'as yet unclassified' } +GrafoscopioNode >> updateEditionTimestamp [ + self edited: DateAndTime now printString +] + { #category : #importing } GrafoscopioNode >> uploadBodyFrom: fileLocator filteredFor: selectedLink [ (self linksFilters contains: selectedLink) diff --git a/src/Grafoscopio/GrafoscopioNodeTest.class.st b/src/Grafoscopio/GrafoscopioNodeTest.class.st index f86983c..7c6ff65 100644 --- a/src/Grafoscopio/GrafoscopioNodeTest.class.st +++ b/src/Grafoscopio/GrafoscopioNodeTest.class.st @@ -60,7 +60,7 @@ GrafoscopioNodeTest >> testHasMarkdownSubtreesToExport [ Please see look #becomeDefaultTestTree message to see the details that makes this test true." | tree | tree := GrafoscopioNode new becomeDefaultTestTree. - self assert: tree selectMarkdownSubtreesToExport isNotEmpty equals: true. + self assert: tree selectMarkupSubtreesToExport isNotEmpty equals: true. ] @@ -69,6 +69,17 @@ GrafoscopioNodeTest >> testInitializeIsOk [ self shouldnt: [ GrafoscopioNode new ] raise: Error ] +{ #category : #tests } +GrafoscopioNodeTest >> testNodeSelection [ + | tree child1 | + tree := GrafoscopioNode new becomeDefaultTestTree. + child1 := tree preorderTraversalRootChildren at: 1. + child1 selected: true. + self assert: tree detectSelectionIndex equals: 1 + + +] + { #category : #tests } GrafoscopioNodeTest >> testPromoteNode [ | tree child1 child2 | @@ -135,3 +146,16 @@ GrafoscopioNodeTest >> testSanitizedLink [ self assert: (node sanitizeDefaultLink = 'https://docutopia.tupale.co/hackbo:hackbot') equals: true ] + +{ #category : #tests } +GrafoscopioNodeTest >> testToggleNodeSelection [ + "I verify that a selected node can be unchosen once a new selection has been done." + + | tree testNode1 testNode2 | + tree := GrafoscopioNode new becomeDefaultTestTree. + testNode1 := (tree preorderTraversalRootChildren at: 1) selected: true. + self assert: tree detectSelectionIndex equals: testNode1 preorderTraversalIndex. + testNode2 := (tree preorderTraversalRootChildren at: 2). + testNode2 toggleSelected. + self assert: tree detectSelectionIndex equals: testNode2 preorderTraversalIndex +] diff --git a/src/Grafoscopio/GrafoscopioNotebook.class.st b/src/Grafoscopio/GrafoscopioNotebook.class.st index d0e38b3..de3f647 100644 --- a/src/Grafoscopio/GrafoscopioNotebook.class.st +++ b/src/Grafoscopio/GrafoscopioNotebook.class.st @@ -72,17 +72,41 @@ GrafoscopioNotebook >> addNode [ self notebookContent: notebook. ] +{ #category : #persistence } +GrafoscopioNotebook >> askToSaveBeforeClosing [ + + | saveChanges | + + saveChanges := UIManager default + question: 'Do you want to save changes in the notebook before closing?' + title: 'Save changes before closing?'. + saveChanges ifNil: [ ^ self notebook unsavedNodes inspect ]. + ^ saveChanges +] + { #category : #operation } GrafoscopioNotebook >> autoSaveBodyOf: aNode [ - | playground | - self body class = GrafoscopioTextModel - ifTrue: [ body body whenTextChanged: [ :arg | aNode body: arg ] ]. - body body class = GlamourPresentationModel + | playground bodyContents | + bodyContents := aNode body. + self body class = GrafoscopioTextModel + ifTrue: [ self body body whenTextChanged: [ :arg | + aNode body: arg. + "self body body whenTextIsAccepted: [:bodyText | + self inform: bodyText. + aNode updateEditionTimestamp ]." + bodyContents = arg ifFalse: [ + "self inform: arg." + "aNode updateEditionTimestamp" ]]]. + self body body class = GlamourPresentationModel ifFalse: [ ^ self ]. - playground := body body glmPres. + playground := self body body glmPres. playground onChangeOfPort: #text - act: [ :x | aNode body: (x pane port: #entity) value content ] + act: [ :x | + aNode body: (x pane port: #entity) value content. + "aNode updateEditionTimestamp." + "self inform: aNode edited" ] + ] { #category : #accessing } @@ -96,7 +120,7 @@ GrafoscopioNotebook >> body: anObject [ ] { #category : #utilities } -GrafoscopioNotebook >> checksum [ +GrafoscopioNotebook >> checksumForRootSubtree [ "I return the checksum (crypto hash) of the workingFile where this notebook is being stored. I'm useful for data provenance and traceability of derivated files coming from this source notebook." @@ -187,9 +211,9 @@ GrafoscopioNotebook >> downloadImages [ ] { #category : #persistence } -GrafoscopioNotebook >> exportAllSubtreesAsMarkdow [ +GrafoscopioNotebook >> exportAllSubtreesAsMarkup [ | toBeExported | - toBeExported := self notebook selectMarkdownSubtreesToExport. + toBeExported := self notebook selectMarkupSubtreesToExport. toBeExported ifEmpty: [ ^ self ]. toBeExported do: [ :each | self subtreeAsMarkdownFileFor: each ]. self inform: toBeExported size asString , ' exported markdown subtrees.' @@ -262,11 +286,13 @@ GrafoscopioNotebook >> exportAsPDF [ { #category : #persistence } GrafoscopioNotebook >> exportAsSton: aNotebook on: aFileStream [ aNotebook flatten. - (STON writer on: aFileStream) + self notebook root updateEditionTimestamp. + (STON writer on: aFileStream) newLine: String crlf; prettyPrint: true; keepNewLines: true; - nextPut: aNotebook children + nextPut: aNotebook children. + ] { #category : #utility } @@ -284,7 +310,7 @@ GrafoscopioNotebook >> exportNode: aGrafoscopioNode asMarkdownIn: aFile [ stream nextPutAll: ('---', String cr, - 'exportedFrom: ', self checksum, String cr) withInternetLineEndings. + 'exportedFrom: ', self checksumForRootSubtree, String cr) withInternetLineEndings. aGrafoscopioNode metadataAsYamlIn: stream. stream nextPutAll: @@ -313,6 +339,11 @@ GrafoscopioNotebook >> findAndReplace [ ] +{ #category : #testing } +GrafoscopioNotebook >> hasAWorkingFileDefined [ + self workingFile ifNil: [ ^ false ] ifNotNil: [ ^ true ] +] + { #category : #accessing } GrafoscopioNotebook >> header [ ^ header @@ -394,8 +425,12 @@ GrafoscopioNotebook >> initializePresenter [ (tree highlightedItem content header) = arg ifFalse: [ tree highlightedItem content header: arg. + tree highlightedItem content updateEditionTimestamp. tree roots: tree roots]]. - links whenTextChanged: [ :arg | tree highlightedItem content addLink: arg ] + links whenTextChanged: [ :arg | + tree highlightedItem content addLink: arg. + tree highlightedItem content updateEditionTimestamp. + ] ] { #category : #initialization } @@ -404,6 +439,7 @@ GrafoscopioNotebook >> initializeWidgets [ header := self newTextInput. header autoAccept: true. body := self newText. + body class logCr. body disable. body text: '<- Select a node'. body autoAccept: true. @@ -420,6 +456,17 @@ GrafoscopioNotebook >> initializeWidgets [ self askOkToClose: true. ] +{ #category : #persistence } +GrafoscopioNotebook >> isSaved [ + "I tell if a notebook has been saved in a persistence storage, including last editions." + ^ self hasAWorkingFileDefined and: [self isSavedAfterLastEdition ]. +] + +{ #category : #testing } +GrafoscopioNotebook >> isSavedAfterLastEdition [ + ^ self notebook isSavedAfterLastEdition +] + { #category : #accessing } GrafoscopioNotebook >> links [ ^ links @@ -630,9 +677,10 @@ GrafoscopioNotebook >> notebookSubMenu [ action: [ self defineDebugMessageUI ] ] ] ] -{ #category : #private } +{ #category : #'event handling' } GrafoscopioNotebook >> okToChange [ - ^ true + + self isSaved ifTrue: [ ^ true ] ifFalse: [ ^ self askToSaveBeforeClosing ] ] { #category : #persistence } @@ -799,7 +847,7 @@ GrafoscopioNotebook >> removeNode [ { #category : #persistence } GrafoscopioNotebook >> saveToFile: aFileReference [ - "I save the current tree/document to a file." + "I save the current tree/document to a file and update storage timestamp." aFileReference ifNil: [ self inform: 'No file selected for saving. Save NOT done.'. ^ self ]. workingFile := aFileReference. @@ -831,6 +879,7 @@ GrafoscopioNotebook >> saveWorkingNotebook [ self workingFile ifNil: [ self saveToFileUI ] ifNotNil: [ self saveToFile: workingFile ]. + self notebook root updateEditionTimestamp. GfUIHelpers updateRecentNotebooksWith: workingFile @@ -891,7 +940,7 @@ GrafoscopioNotebook >> topBar [ name: nil; description: 'Export all Markdown subtrees'; icon: (self iconNamed: #glamorousMore); - action: [ self exportAllSubtreesAsMarkdow ] ]. + action: [ self exportAllSubtreesAsMarkup ] ]. group addItem: [ :item | item @@ -1062,6 +1111,7 @@ GrafoscopioNotebook >> updateBodyFor: aNodeContainer [ tree needRebuild: false. body needRebuild: true. aNode := aNodeContainer content. + aNode toggleSelected. header text: aNode header. body := self instantiate: aNode specModelClass new. body content: aNode body. diff --git a/src/Grafoscopio/GrafoscopioTextModel.class.st b/src/Grafoscopio/GrafoscopioTextModel.class.st index e9dc19a..6bcd3f8 100644 --- a/src/Grafoscopio/GrafoscopioTextModel.class.st +++ b/src/Grafoscopio/GrafoscopioTextModel.class.st @@ -4,7 +4,7 @@ Usually my content is markdown text. " Class { #name : #GrafoscopioTextModel, - #superclass : #ComposableModel, + #superclass : #ComposablePresenter, #instVars : [ 'body' ],