diff --git a/repository/Grafoscopio/GrafoscopioNode.class.st b/repository/Grafoscopio/GrafoscopioNode.class.st
index 1c6c178..28ef407 100644
--- a/repository/Grafoscopio/GrafoscopioNode.class.st
+++ b/repository/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/repository/Grafoscopio/GrafoscopioNodeTest.class.st b/repository/Grafoscopio/GrafoscopioNodeTest.class.st
index f86983c..7c6ff65 100644
--- a/repository/Grafoscopio/GrafoscopioNodeTest.class.st
+++ b/repository/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/repository/Grafoscopio/GrafoscopioNotebook.class.st b/repository/Grafoscopio/GrafoscopioNotebook.class.st
index d0e38b3..de3f647 100644
--- a/repository/Grafoscopio/GrafoscopioNotebook.class.st
+++ b/repository/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/repository/Grafoscopio/GrafoscopioTextModel.class.st b/repository/Grafoscopio/GrafoscopioTextModel.class.st
index e9dc19a..6bcd3f8 100644
--- a/repository/Grafoscopio/GrafoscopioTextModel.class.st
+++ b/repository/Grafoscopio/GrafoscopioTextModel.class.st
@@ -4,7 +4,7 @@ Usually my content is markdown text.
"
Class {
#name : #GrafoscopioTextModel,
- #superclass : #ComposableModel,
+ #superclass : #ComposablePresenter,
#instVars : [
'body'
],