/
App.kt
254 lines (232 loc) · 7.1 KB
/
App.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
package example
import java.awt.*
import javax.swing.*
import javax.swing.event.TreeExpansionEvent
import javax.swing.event.TreeWillExpandListener
import javax.swing.plaf.basic.BasicTreeUI
import javax.swing.text.html.HTML
import javax.swing.text.html.HTMLDocument
import javax.swing.text.html.HTMLEditorKit
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.ExpandVetoException
import javax.swing.tree.TreePath
import javax.swing.tree.TreeSelectionModel
private const val HTML_TEXT = """
<html>
<body>
<h1>Scrollspy</h1>
<p id='main'></p>
<p id='bottom'>id=bottom</p>
</body>
</html>
"""
private val editor = JEditorPane()
fun makeUI(): Component {
val emptyIcon = EmptyIcon()
UIManager.put("Tree.openIcon", emptyIcon)
UIManager.put("Tree.closedIcon", emptyIcon)
UIManager.put("Tree.leafIcon", emptyIcon)
UIManager.put("Tree.expandedIcon", emptyIcon)
UIManager.put("Tree.collapsedIcon", emptyIcon)
UIManager.put("Tree.leftChildIndent", 10)
UIManager.put("Tree.rightChildIndent", 0)
UIManager.put("Tree.paintLines", false)
val htmlEditorKit = HTMLEditorKit()
editor.isEditable = false
editor.editorKit = htmlEditorKit
editor.text = HTML_TEXT
val doc = editor.document as? HTMLDocument
val element = doc?.getElement("main")
val model = makeModel()
(model.root as? DefaultMutableTreeNode)?.preorderEnumeration()?.toList()
?.filterIsInstance<DefaultMutableTreeNode>()
?.filterNot { it.isRoot }
?.map { it.userObject }
?.forEach {
val tag = "<a name='$it' href='#'>$it</a>" + "<br />".repeat(12)
runCatching {
doc?.insertBeforeEnd(element, tag)
}.onFailure {
UIManager.getLookAndFeel().provideErrorFeedback(editor)
}
}
val tree = makeTree()
tree.model = model
tree.addTreeSelectionListener { e ->
val n = e.newLeadSelectionPath.lastPathComponent
if (tree.isEnabled && n is DefaultMutableTreeNode) {
editor.scrollToReference(n.userObject.toString())
}
}
// scroll to top of page
EventQueue.invokeLater { editor.scrollRectToVisible(editor.bounds) }
val scroll = JScrollPane(editor)
scroll.verticalScrollBar.model.addChangeListener {
val itr = doc?.getIterator(HTML.Tag.A)
while (itr?.isValid == true) {
val r = runCatching {
editor.modelToView(itr.startOffset)
}.getOrNull()
if (r != null && editor.visibleRect.contains(r.location)) {
searchTreeNode(tree, itr.attributes.getAttribute(HTML.Attribute.NAME))
break
}
itr.next()
}
}
return JSplitPane(JSplitPane.HORIZONTAL_SPLIT, JScrollPane(tree), scroll).also {
it.resizeWeight = .5
it.preferredSize = Dimension(320, 240)
}
}
fun makeTree(): JTree {
val tree = RowSelectionTree()
tree.rowHeight = 32
tree.border = BorderFactory.createEmptyBorder(2, 2, 2, 2)
tree.selectionModel.selectionMode = TreeSelectionModel.SINGLE_TREE_SELECTION
expandAllNodes(tree)
return tree
}
// https://ateraimemo.com/Swing/ExpandAllNodes.html
private fun expandAllNodes(tree: JTree) {
var row = 0
while (row < tree.rowCount) {
tree.expandRow(row++)
}
}
private fun searchTreeNode(
tree: JTree,
name: Any,
) {
val model = tree.model
val root = model.root as? DefaultMutableTreeNode ?: return
root.preorderEnumeration().toList()
.filterIsInstance<DefaultMutableTreeNode>()
.firstOrNull { name == it.userObject.toString() }
?.also {
tree.isEnabled = false
val path = TreePath(it.path)
tree.selectionPath = path
tree.scrollPathToVisible(path)
tree.isEnabled = true
}
}
private fun makeModel(): DefaultTreeModel {
val root = DefaultMutableTreeNode("root")
val c1 = DefaultMutableTreeNode("1. Introduction")
root.add(c1)
val c2 = DefaultMutableTreeNode("2. Chapter")
c2.add(DefaultMutableTreeNode("2.1. Section"))
c2.add(DefaultMutableTreeNode("2.2. Section"))
c2.add(DefaultMutableTreeNode("2.3. Section"))
root.add(c2)
val c3 = DefaultMutableTreeNode("3. Chapter")
c3.add(DefaultMutableTreeNode("3.1. Section"))
c3.add(DefaultMutableTreeNode("3.2. Section"))
c3.add(DefaultMutableTreeNode("3.3. Section"))
c3.add(DefaultMutableTreeNode("3.4. Section"))
root.add(c3)
return DefaultTreeModel(root)
}
private class RowSelectionTree : JTree() {
@Transient private var listener: TreeWillExpandListener? = null
override fun paintComponent(g: Graphics) {
val sr = selectionRows
if (sr == null) {
super.paintComponent(g)
return
}
g.color = background
g.fillRect(0, 0, width, height)
val g2 = g.create() as? Graphics2D ?: return
g2.paint = SELECTED_COLOR
sr.map { getRowBounds(it) }.forEach { g2.fillRect(0, it.y, width, it.height) }
super.paintComponent(g)
if (hasFocus()) {
leadSelectionPath?.also {
val r = getRowBounds(getRowForPath(it))
g2.paint = SELECTED_COLOR.darker()
g2.drawRect(0, r.y, width - 1, r.height - 1)
}
}
g2.dispose()
}
override fun updateUI() {
setCellRenderer(null)
removeTreeWillExpandListener(listener)
super.updateUI()
val tmp = object : BasicTreeUI() {
override fun getPathBounds(tree: JTree?, path: TreePath?) =
tree?.let {
getPathBounds(path, it.insets, Rectangle())
}
private fun getPathBounds(path: TreePath?, insets: Insets, bounds: Rectangle) =
treeState?.getBounds(path, bounds)?.also {
it.width = tree.width
it.y += insets.top
}
}
setUI(tmp)
UIManager.put("Tree.repaintWholeRow", true)
val r = getCellRenderer()
setCellRenderer { tree, value, selected, expanded, leaf, row, hasFocus ->
r.getTreeCellRendererComponent(
tree,
value,
selected,
expanded,
leaf,
row,
hasFocus,
).also {
it.background = if (selected) SELECTED_COLOR else tree.background
(it as? JComponent)?.isOpaque = true
}
}
isOpaque = false
isRootVisible = false
listener = object : TreeWillExpandListener {
override fun treeWillExpand(e: TreeExpansionEvent) { // throws ExpandVetoException {
// throw ExpandVetoException(e, "Tree expansion cancelled")
}
@Throws(ExpandVetoException::class)
override fun treeWillCollapse(e: TreeExpansionEvent) {
throw ExpandVetoException(e, "Tree collapse cancelled")
}
}
addTreeWillExpandListener(listener)
}
companion object {
private val SELECTED_COLOR = Color(0x64_96_C8)
}
}
private class EmptyIcon : Icon {
override fun paintIcon(
c: Component?,
g: Graphics,
x: Int,
y: Int,
) {
// Empty icon
}
override fun getIconWidth() = 0
override fun getIconHeight() = 0
}
fun main() {
EventQueue.invokeLater {
runCatching {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
}.onFailure {
it.printStackTrace()
Toolkit.getDefaultToolkit().beep()
}
JFrame().apply {
defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
contentPane.add(makeUI())
pack()
setLocationRelativeTo(null)
isVisible = true
}
}
}