Skip to content

Commit 478c9a1

Browse files
committed
Add ScijavaReplFXTabs
1 parent 09b4e18 commit 478c9a1

File tree

3 files changed

+175
-115
lines changed

3 files changed

+175
-115
lines changed

src/main/kotlin/org/scijava/scripting/fx/SciJavaReplFX.kt

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,61 +29,67 @@ private fun invokeOnFXApplicationThread(task: Runnable) {
2929

3030
class SciJavaReplFX(context: Context) {
3131

32-
private val history = TextArea("")
33-
private val prompt = TextArea("")
32+
private val _history = TextArea("")
33+
private val _prompt = TextArea("")
3434
private val progress = ProgressIndicator(1.0)
3535
private val progressGroup = Group(progress)
36-
private val historyStack = StackPane(history, progressGroup)
37-
private val box = VBox(historyStack, prompt)
38-
private val stream = PrintToStringConsumerStream { invokeOnFXApplicationThread { history.text = "${history.text}$it" } }
36+
private val historyStack = StackPane(_history, progressGroup)
37+
private val box = VBox(historyStack, _prompt)
38+
private val stream = PrintToStringConsumerStream { invokeOnFXApplicationThread { _history.text = "${_history.text}$it" } }
3939
private val repl = ScriptREPL(context, stream)
4040
private var count = 0
4141

4242
init {
4343

4444

4545
box.maxWidth = Double.POSITIVE_INFINITY
46-
history.maxWidth = Double.POSITIVE_INFINITY
47-
prompt.maxWidth = Double.POSITIVE_INFINITY
46+
_history.maxWidth = Double.POSITIVE_INFINITY
47+
_prompt.maxWidth = Double.POSITIVE_INFINITY
4848
historyStack.maxWidth = Double.POSITIVE_INFINITY
4949

5050
progress.prefWidth = 50.0
5151

52-
history.isWrapText = false
53-
prompt.isWrapText = false
52+
_history.isWrapText = false
53+
_prompt.isWrapText = false
5454

55-
prompt.promptText = "In [$count]:"
55+
_prompt.promptText = "In [$count]:"
5656

5757
box.isFillWidth = true
5858

59-
history.maxHeight = Double.POSITIVE_INFINITY
60-
if (repl.interpreter === null) {
59+
_history.maxHeight = Double.POSITIVE_INFINITY
60+
if (repl.interpreter === null)
6161
repl.lang(repl.interpretedLanguages.first())
62-
}
6362

64-
history.isEditable = false
65-
prompt.isEditable = true
63+
_history.isEditable = false
64+
_prompt.isEditable = true
6665

67-
history.font = Font.font("Monospace")
68-
prompt.font = Font.font("Monospace")
66+
_history.font = Font.font("Monospace")
67+
_prompt.font = Font.font("Monospace")
6968

7069
VBox.setVgrow(historyStack, Priority.ALWAYS)
7170
StackPane.setAlignment(progressGroup, Pos.BOTTOM_RIGHT)
7271
StackPane.setMargin(progressGroup, Insets(10.0))
7372

7473
repl.initialize(true)
7574

76-
prompt.prefWidthProperty().addListener( InvalidationListener { prompt.maxHeight = prompt.prefHeight } )
77-
prompt.editableProperty().addListener { obs, oldv, newv -> progress.progress = if (newv) 1.0 else -1.0 }
78-
progress.visibleProperty().bind(prompt.editableProperty().not())
75+
_prompt.prefWidthProperty().addListener( InvalidationListener { _prompt.maxHeight = _prompt.prefHeight } )
76+
_prompt.editableProperty().addListener { _, _, new -> progress.progress = if (new) 1.0 else -1.0 }
77+
progress.visibleProperty().bind(_prompt.editableProperty().not())
7978

8079
}
8180

82-
fun getNode(): Node = box
81+
val node: Node
82+
get() = box
83+
84+
val prompt: Node
85+
get() = _prompt
86+
87+
val history: Node
88+
get() = _history
8389

8490
fun <T: Event> addPromptEventHandler(
8591
eventType: EventType<T>,
86-
eventHandler: EventHandler<T>) = prompt.addEventHandler(eventType, eventHandler)
92+
eventHandler: EventHandler<T>) = _prompt.addEventHandler(eventType, eventHandler)
8793

8894

8995
fun <T: Event> addPromptEventHandler(
@@ -92,41 +98,41 @@ class SciJavaReplFX(context: Context) {
9298

9399
@Synchronized
94100
fun evalCurrentPrompt() {
95-
val promptText = prompt.text
101+
val promptText = _prompt.text
96102
invokeOnFXApplicationThread {
97-
prompt.text = ""
98-
prompt.positionCaret(0)
99-
prompt.promptText = "In [${count + 1}]:"
100-
prompt.isEditable = false
101-
history.text = "${history.text}\nIn [$count]: ${promptText}\nOut [$count]: "
102-
history.positionCaret(history.text.length)
103+
_prompt.text = ""
104+
_prompt.positionCaret(0)
105+
_prompt.promptText = "In [${count + 1}]:"
106+
_prompt.isEditable = false
107+
_history.text = "${_history.text}\nIn [$count]: ${promptText}\nOut [$count]: "
108+
_history.positionCaret(_history.text.length)
103109
}
104110
// TODO use queue or block this thread here? could potentially block main application thread? bad?
105111
try {
106112
repl.evaluate(promptText)
107113
} finally {
108114
++count
109115
invokeOnFXApplicationThread {
110-
prompt.isEditable = true
111-
history.positionCaret(history.text.length)
116+
_prompt.isEditable = true
117+
_history.positionCaret(_history.text.length)
112118
}
113119
}
114120
}
115121

116122
private fun scaleFontSisze(factor: Double) {
117123
require(factor > 0, { "Factor > 0 required but received $factor <= 0" })
118-
val size = history.font.size
124+
val size = _history.font.size
119125
val font = Font.font("Monospace", size * factor)
120-
history.font = font
121-
prompt.font = font
126+
_history.font = font
127+
_prompt.font = font
122128
}
123129

124130
fun increaseFontSize(factor: Double = 1.1) = scaleFontSisze(factor)
125131

126132
fun decreaseFontSize(factor: Double = 1.1) = scaleFontSisze(1.0 / factor)
127133

128134
fun setPromptPrefHeight(height: Double) {
129-
prompt.prefHeight = height
135+
_prompt.prefHeight = height
130136
}
131137

132138

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package org.scijava.scripting.fx
2+
3+
import javafx.event.EventHandler
4+
import javafx.scene.Node
5+
import javafx.scene.control.Tab
6+
import javafx.scene.control.TabPane
7+
import javafx.scene.input.KeyCode
8+
import javafx.scene.input.KeyCodeCombination
9+
import javafx.scene.input.KeyCombination
10+
import javafx.scene.input.KeyEvent
11+
import org.scijava.Context
12+
13+
class ScijavaReplFXTabs(
14+
private val context: Context,
15+
private val increaseFontKeys: Collection<KeyCombination> = setOf(KeyCodeCombination(KeyCode.EQUALS, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_ANY)),
16+
private val decreaseFontKeys: Collection<KeyCombination> = setOf(KeyCodeCombination(KeyCode.MINUS, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_ANY)),
17+
private val evalKeys: Collection<KeyCombination> = setOf(KeyCodeCombination(KeyCode.ENTER, KeyCombination.CONTROL_DOWN)),
18+
private val exitKeyCombination: Collection<KeyCombination> = setOf (KeyCodeCombination(KeyCode.D, KeyCombination.CONTROL_DOWN)),
19+
private val createNewReplCombination: Collection<KeyCombination> = setOf(KeyCodeCombination(KeyCode.T, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN)),
20+
private val cycleTabsForwardKombination: Collection<KeyCombination> = setOf(KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN)),
21+
private val cycleTabsBackwardKombination: Collection<KeyCombination> = setOf(KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN))
22+
) {
23+
24+
25+
private val tabPane = TabPane()
26+
.also { it.tabClosingPolicy = TabPane.TabClosingPolicy.ALL_TABS }
27+
28+
val node: Node
29+
get() = tabPane
30+
31+
private val replIds = HashMap<Int, Pair<SciJavaReplFX, Tab>>()
32+
33+
init {
34+
tabPane.addEventHandler(KeyEvent.KEY_PRESSED)
35+
{ ev ->
36+
if (createNewReplCombination.any { it.match(ev) }) {
37+
ev.consume()
38+
createAndAddTab()
39+
}
40+
}
41+
}
42+
43+
fun createAndAddTab() {
44+
val repl = SciJavaReplFX(context)
45+
46+
repl.setPromptPrefHeight(250.0)
47+
48+
repl.node.addEventHandler(KeyEvent.KEY_PRESSED) {
49+
if (increaseFontKeys.any { c -> c.match(it) }) {
50+
it.consume()
51+
repl.increaseFontSize()
52+
} else if (decreaseFontKeys.any { c -> c.match(it) }) {
53+
it.consume()
54+
repl.decreaseFontSize()
55+
}
56+
}
57+
58+
repl.prompt.addEventHandler(KeyEvent.KEY_PRESSED) {
59+
when {
60+
evalKeys.any { c -> c.match(it) } -> {
61+
it.consume()
62+
// TODO use coroutines instead of thread
63+
Thread { repl.evalCurrentPrompt() }.start()
64+
}
65+
cycleTabsForwardKombination.any { c -> c.match(it) } -> {
66+
it.consume()
67+
cycleForward()
68+
}
69+
cycleTabsBackwardKombination.any { c -> c.match(it) } -> {
70+
it.consume()
71+
cycleBackward()
72+
}
73+
}
74+
}
75+
76+
synchronized(replIds) {
77+
val replId = addTab(repl)
78+
repl.prompt.addEventHandler(KeyEvent.KEY_PRESSED) {
79+
if (exitKeyCombination.any { c -> c.match(it) }) {
80+
it.consume()
81+
removeTab(replId)
82+
}
83+
}
84+
}
85+
}
86+
87+
@Synchronized
88+
private fun addTab(repl: SciJavaReplFX): Int {
89+
val replId = smallestId(replIds.keys)
90+
Tab("REPL $replId", repl.node)
91+
.also { it.selectedProperty().addListener { _, _, new -> if (new) repl.prompt.requestFocus() } }
92+
.also { replIds[replId] = Pair(repl, it) }
93+
.also { it.onClosed = EventHandler { _ -> removeTab(it) } }
94+
.also { tabPane.tabs.add(it) }
95+
.also { tabPane.selectionModel.select(it) }
96+
return replId
97+
}
98+
99+
@Synchronized
100+
private fun removeTab(replId: Int) = replIds.remove(replId)?.second?.let { tabPane.tabs.remove(it) }
101+
102+
@Synchronized
103+
private fun removeTab(tab: Tab) {
104+
tabPane.tabs.remove(tab)
105+
replIds.filterValues { it.second === tab }.keys.map { it }.forEach { replIds.remove(it) }
106+
}
107+
108+
@Synchronized
109+
private fun smallestId(set: Set<Int>, min: Int = 1): Int {
110+
var start = min
111+
while (set.contains(start)) ++start
112+
return start
113+
}
114+
115+
private fun cycleForward() {
116+
tabPane.selectionModel.let { sm ->
117+
val tab = sm.selectedItem
118+
sm.selectNext()
119+
if (tab === sm.selectedItem)
120+
sm.selectFirst()
121+
}
122+
}
123+
124+
private fun cycleBackward() {
125+
tabPane.selectionModel.let { sm ->
126+
val tab = sm.selectedItem
127+
sm.selectPrevious()
128+
if (tab === sm.selectedItem)
129+
sm.selectLast()
130+
}
131+
}
132+
}

src/test/kotlin/org/scijava/scripting/fx/SciJavaReplFXExample.kt

Lines changed: 2 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -2,102 +2,24 @@ package org.scijava.scripting.fx
22

33
import com.sun.javafx.application.PlatformImpl
44
import javafx.application.Platform
5-
import javafx.collections.FXCollections
6-
import javafx.collections.MapChangeListener
75
import javafx.scene.Scene
8-
import javafx.scene.control.Tab
9-
import javafx.scene.control.TabPane
10-
import javafx.scene.input.KeyCode
11-
import javafx.scene.input.KeyCodeCombination
12-
import javafx.scene.input.KeyCombination
13-
import javafx.scene.input.KeyEvent
146
import javafx.scene.layout.StackPane
157
import javafx.stage.Stage
16-
import kotlinx.coroutines.GlobalScope
17-
import kotlinx.coroutines.launch
188
import org.scijava.Context
199

20-
private fun smallestId(set: Set<Int>, min: Int = 1): Int {
21-
var start = min
22-
while (set.contains(start)) ++start
23-
return start
24-
}
25-
2610
fun main() {
2711

2812
val context = Context()
2913
PlatformImpl.startup {}
3014

31-
val increaseFontKeys = setOf(KeyCodeCombination(KeyCode.EQUALS, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_ANY))
32-
val decreaseFontKeys = setOf(KeyCodeCombination(KeyCode.MINUS, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_ANY))
33-
val evalKeys = setOf(KeyCodeCombination(KeyCode.ENTER, KeyCombination.CONTROL_DOWN))
34-
val exitKeyCombination = setOf(KeyCodeCombination(KeyCode.D, KeyCombination.CONTROL_DOWN))
35-
15+
val tabPane = ScijavaReplFXTabs(context).also { it.createAndAddTab() }
3616

37-
val tabPane = TabPane()
38-
39-
val root = StackPane(tabPane)
17+
val root = StackPane(tabPane.node)
4018
Platform.runLater {
4119
val scene = Scene(root, 800.0, 600.0)
4220
val stage = Stage()
4321
stage.scene = scene
4422
stage.show()
4523
}
4624

47-
val replIds = FXCollections.observableHashMap<Int, Pair<SciJavaReplFX, Tab>>()
48-
replIds.addListener(MapChangeListener<Int, Pair<SciJavaReplFX, Tab>> {
49-
if (it.wasRemoved())
50-
tabPane.tabs.remove(it.valueRemoved.second)
51-
else if (it.wasAdded()) {
52-
tabPane.tabs.add(it.valueAdded.second)
53-
tabPane.selectionModel.select(it.valueAdded.second)
54-
}
55-
})
56-
57-
val createAndAddRepl = {
58-
val repl = SciJavaReplFX(context)
59-
60-
repl.setPromptPrefHeight(250.0)
61-
62-
repl.getNode().addEventHandler(KeyEvent.KEY_PRESSED) {
63-
if (increaseFontKeys.any { c -> c.match(it) }) {
64-
it.consume()
65-
repl.increaseFontSize()
66-
} else if (decreaseFontKeys.any { c -> c.match(it) }) {
67-
it.consume()
68-
repl.decreaseFontSize()
69-
}
70-
}
71-
72-
repl.addPromptEventHandler(KeyEvent.KEY_PRESSED) {
73-
if (evalKeys.any { c -> c.match(it) }) {
74-
it.consume()
75-
GlobalScope.launch { repl.evalCurrentPrompt() }
76-
}
77-
}
78-
synchronized(replIds) {
79-
val replId = smallestId(replIds.keys)
80-
replIds[replId] = Pair(repl, Tab("REPL $replId", repl.getNode()))
81-
82-
repl.addPromptEventHandler(KeyEvent.KEY_PRESSED) {
83-
if (exitKeyCombination.any { c -> c.match(it) }) {
84-
it.consume()
85-
synchronized(replIds) {
86-
replIds.remove(replId)
87-
}
88-
}
89-
}
90-
}
91-
}
92-
93-
createAndAddRepl()
94-
95-
tabPane.addEventHandler(KeyEvent.KEY_PRESSED) {
96-
if (KeyCodeCombination(KeyCode.T, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN).match(it)) {
97-
it.consume()
98-
createAndAddRepl()
99-
}
100-
}
101-
102-
10325
}

0 commit comments

Comments
 (0)