0
|
1 /*
|
|
2 * The queue of pasteboard items we manage. New stuff gets added to the
|
|
3 * tail, and old stuff truncated off the head.
|
|
4 */
|
|
5 package name.blackcap.clipman
|
|
6
|
|
7 import java.awt.Container
|
1
|
8 import java.awt.Rectangle
|
0
|
9 import java.util.Collections
|
|
10 import java.util.LinkedList
|
1
|
11 import java.util.logging.Level
|
|
12 import java.util.logging.Logger
|
0
|
13 import javax.swing.*
|
27
|
14 import javax.swing.text.DefaultHighlighter
|
0
|
15
|
|
16 /**
|
|
17 * A queue that tracks the data we display and the widgets used to
|
|
18 * display them. We never explicitly remove stuff from the queue,
|
|
19 * though items will get silently discarded to prevent the queue from
|
|
20 * exceeding the specified maximum size.
|
|
21 */
|
|
22 class PasteboardQueue(val parent: Container, maxSize: Int) {
|
|
23 private val queue = LinkedList<QueueItem>()
|
|
24 private var _maxSize = maxSize
|
16
|
25 private var scrollPane: JScrollPane? = null
|
|
26 init {
|
|
27 var sp: Container? = parent
|
|
28 while (sp != null) {
|
|
29 if (sp is JScrollPane) {
|
|
30 scrollPane = sp
|
|
31 break
|
|
32 }
|
|
33 sp = sp.parent
|
|
34 }
|
|
35 }
|
0
|
36
|
29
|
37 data class Offset(val inQueue: Int, val inItem: Int)
|
21
|
38 enum class Direction { FORWARDS, BACKWARDS }
|
|
39
|
0
|
40 /**
|
|
41 * The maximum allowed size of this queue. Attempts to make the queue
|
|
42 * larger than this size, or specifying a size smaller than the current
|
|
43 * size, will result in the oldest item(s) being discarded. A size less
|
|
44 * than or equal to zero means an unlimited size.
|
|
45 */
|
|
46 var maxSize: Int
|
41
|
47 get() { return _maxSize }
|
|
48 @Synchronized set(value) {
|
|
49 _maxSize = value
|
|
50 truncate()
|
|
51 }
|
0
|
52
|
|
53 /**
|
1
|
54 * Add a QueueItem to the end of the queue.
|
|
55 * @param item QueueItem to add
|
0
|
56 */
|
|
57 @Synchronized fun add(item: QueueItem) {
|
1
|
58 inSwingThread {
|
27
|
59 parent.add(item.view.contents)
|
46
|
60 validate()
|
1
|
61 }
|
0
|
62 queue.addLast(item)
|
5
|
63 truncate()
|
0
|
64 }
|
|
65
|
21
|
66 /**
|
|
67 * Find and highlight the next occurrence of the specified string
|
|
68 * @param string to search
|
|
69 * @param whether to search backwards (default forwards)
|
|
70 * @param case-folding flag (default true)
|
|
71 * @param starting point (0, 0) for forwards, (m, n) for backwards
|
|
72 * @return position where start of string was found, or null
|
|
73 */
|
|
74 fun find(needle: String, direction: Direction = Direction.FORWARDS,
|
|
75 foldCase: Boolean = true, origin: Offset? = null): Offset?
|
|
76 {
|
27
|
77 /* clean up any old highlights */
|
|
78 queue.forEach {
|
|
79 val hiliter = it.view.searchable.highlighter
|
|
80 hiliter.highlights.forEach {
|
|
81 hiliter.removeHighlight(it)
|
21
|
82 }
|
27
|
83 }
|
|
84
|
|
85 /* get starting item index */
|
|
86 val qMax = queue.size
|
|
87 var norigin = origin ?: when (direction) {
|
|
88 Direction.FORWARDS -> Offset(0, 0)
|
|
89 Direction.BACKWARDS -> Offset(qMax - 1, -1)
|
21
|
90 }
|
|
91
|
27
|
92 /* loop initialization */
|
|
93 val (start, incr, search) = if (direction == Direction.FORWARDS) {
|
|
94 Triple( 0, 1, { n: String, h: String, o: Int -> h.indexOf(n, o, foldCase) })
|
|
95 } else {
|
|
96 Triple(-1, -1, { n: String, h: String, o: Int -> h.lastIndexOf(n, o, foldCase) })
|
|
97 }
|
|
98 val painter = DefaultHighlighter.DefaultHighlightPainter(null);
|
|
99 var pos = -1
|
|
100
|
|
101 /* try and find it */
|
|
102 while (norigin.inQueue >= 0 && norigin.inQueue < qMax) {
|
|
103 val si = queue.get(norigin.inQueue).view.searchable
|
|
104 val doc = si.document
|
|
105 val text = doc.getText(0, doc.length)
|
|
106 pos = if (norigin.inItem >= 0) norigin.inItem else text.length - 1
|
|
107 pos = search(needle, text, pos)
|
|
108 if (pos >= 0) {
|
|
109 si.highlighter.addHighlight(pos, pos+needle.length, painter)
|
40
|
110 val r = si.modelToView(pos).apply {
|
|
111 add(si.modelToView(pos + needle.length - 1))
|
|
112 }
|
|
113 si.scrollRectToVisible(r)
|
27
|
114 break
|
|
115 }
|
|
116 norigin = Offset(norigin.inQueue + incr, start)
|
|
117 }
|
|
118 return if (pos >= 0) Offset(norigin.inQueue, pos) else null
|
|
119 }
|
|
120
|
|
121 /**
|
|
122 * Ensure none of the searchables in this queue are selected.
|
|
123 */
|
|
124 fun deselectAll() {
|
|
125 queue.forEach {
|
|
126 val s = it.view.searchable as? ClipText
|
|
127 if (s != null && s.selected) {
|
|
128 s.selected = false
|
|
129 s.validate()
|
|
130 }
|
|
131 }
|
|
132 }
|
|
133
|
|
134 /**
|
|
135 * Return the selected item, or null if nothing has been selected
|
|
136 */
|
|
137 fun getSelected(): QueueItem? {
|
|
138 queue.forEach {
|
|
139 if ((it.view.searchable as? ClipText)?.selected ?: false) {
|
|
140 return it
|
|
141 }
|
|
142 }
|
21
|
143 return null
|
|
144 }
|
|
145
|
5
|
146 private fun truncate() {
|
0
|
147 if (_maxSize > 0) {
|
|
148 var size = queue.size
|
5
|
149 var dirty = false
|
0
|
150 while (size > _maxSize) {
|
27
|
151 var extra = queue.removeFirst().view
|
|
152 inSwingThread {
|
|
153 if (extra.searchable.selected) {
|
47
|
154 Application.anyRequired.disable()
|
|
155 Application.styledRequired.disable()
|
27
|
156 }
|
|
157 parent.remove(extra.contents)
|
|
158 }
|
0
|
159 dirty = true
|
|
160 size -= 1
|
|
161 }
|
|
162 if (dirty) {
|
46
|
163 inSwingThread { validate() }
|
|
164 }
|
|
165 }
|
|
166 }
|
|
167
|
|
168 private fun validate()
|
|
169 {
|
|
170 if (scrollPane == null) {
|
|
171 parent.validate()
|
|
172 } else {
|
|
173 scrollPane!!.run {
|
|
174 validate()
|
|
175 verticalScrollBar.run { value = maximum + 1 }
|
0
|
176 }
|
|
177 }
|
|
178 }
|
|
179 }
|
|
180
|
|
181 /**
|
27
|
182 * An item in the above queue. Linking model to view here sorta violates
|
|
183 * MVC principles, but rules are sometimes best broken. Doing it this way
|
|
184 * makes it impossible for the view queue to fail to follow the data
|
|
185 * queue.
|
0
|
186 */
|
27
|
187 data class QueueItem(val contents: PasteboardItem, val view: PasteboardItemView)
|