1 /* |
1 /* |
2 * ScrollerDockable.scala |
2 * ScrollerDockable.scala |
3 * |
3 * |
4 * TODO: clearer seperation of tasks: layout message_view, layout panel (with preferredsize), repaint, relayout, ... |
4 * TODO: |
5 * + relayout on ValueIsAdjusting while scrolling |
|
6 * + scrolling *one* panel |
5 * + scrolling *one* panel |
7 */ |
6 */ |
8 |
7 |
9 package isabelle.jedit |
8 package isabelle.jedit |
10 |
9 |
11 import scala.collection.mutable.ArrayBuffer |
10 import scala.collection.mutable.{ArrayBuffer, HashMap} |
12 |
11 |
13 import java.awt.{ BorderLayout, Adjustable } |
12 import java.awt.{ BorderLayout, Adjustable } |
14 import java.awt.event.{ ActionListener, ActionEvent, AdjustmentListener, AdjustmentEvent, ComponentListener, ComponentEvent } |
13 import java.awt.event.{ ActionListener, ActionEvent, AdjustmentListener, AdjustmentEvent, ComponentListener, ComponentEvent } |
15 import javax.swing.{ JPanel, JRadioButton, JScrollBar, JTextArea, Timer } |
14 import javax.swing.{ JFrame, JPanel, JRadioButton, JScrollBar, JTextArea, Timer } |
16 |
15 |
17 import org.w3c.dom.Document |
16 import org.w3c.dom.Document |
18 |
17 |
19 import org.xhtmlrenderer.simple.XHTMLPanel |
18 import org.xhtmlrenderer.simple.XHTMLPanel |
20 import org.xhtmlrenderer.context.AWTFontResolver |
19 import org.xhtmlrenderer.context.AWTFontResolver |
21 |
20 |
22 import org.gjt.sp.jedit.View |
21 import org.gjt.sp.jedit.View |
23 |
22 |
24 class ScrollerDockable(view : View, position : String) extends JPanel with AdjustmentListener with ComponentListener { |
23 trait Unrendered[T] { |
25 //holding the unrendered messages |
24 def addUnrendered (id: Int, u: T) : Unit |
26 val message_buffer = new ArrayBuffer[Document]() |
25 def getUnrendered (id: Int) : Option[T] |
27 //rendered messages |
26 def size : Int |
28 var message_cache = Map[Int, XHTMLPanel]() |
27 } |
29 // defining the current view |
28 |
30 val scrollunit = 1 |
29 trait Rendered[T] { |
31 var message_offset = 0 //how many pixels of the lowest message are hidden |
30 def getRendered (id: Int) : Option[T] |
32 var message_no = -1 //index of the lowest message |
31 } |
33 // absolute positioning for messages |
32 |
34 val message_panel = new JPanel |
33 object Renderer { |
35 message_panel.setLayout(null) |
|
36 // setting up view |
|
37 setLayout(new BorderLayout()) |
|
38 val vscroll = new JScrollBar(Adjustable.VERTICAL, 0, 1, 0, 1) |
|
39 vscroll.addAdjustmentListener(this) |
|
40 add (vscroll, BorderLayout.EAST) |
|
41 add (message_panel, BorderLayout.CENTER) |
|
42 addComponentListener(this) |
|
43 //automated scrolling, new message ind |
|
44 val infopanel = new JPanel |
|
45 val auto_scroll = new JRadioButton("Auto Scroll", false) |
|
46 //indicates new messages with a new color, and shows number of messages in cache |
|
47 val message_ind = new JTextArea("0") |
|
48 infopanel.add(message_ind) |
|
49 infopanel.add(auto_scroll) |
|
50 add (infopanel, BorderLayout.SOUTH) |
|
51 |
|
52 // DoubleBuffering for smoother updates |
|
53 this.setDoubleBuffered(true) |
|
54 message_panel.setDoubleBuffered(true) |
|
55 |
|
56 //Render a message to a Panel |
|
57 def render (message: Document): XHTMLPanel = { |
34 def render (message: Document): XHTMLPanel = { |
58 val panel = new XHTMLPanel(new UserAgent()) |
35 val panel = new XHTMLPanel(new UserAgent()) |
59 val fontResolver = |
36 val fontResolver = |
60 panel.getSharedContext.getFontResolver.asInstanceOf[AWTFontResolver] |
37 panel.getSharedContext.getFontResolver.asInstanceOf[AWTFontResolver] |
61 if (Plugin.plugin.viewFont != null) |
38 if (Plugin.plugin.viewFont != null) |
62 fontResolver.setFontMapping("Isabelle", Plugin.plugin.viewFont) |
39 fontResolver.setFontMapping("Isabelle", Plugin.plugin.viewFont) |
63 |
40 |
64 Plugin.plugin.viewFontChanged.add(font => { |
41 Plugin.plugin.viewFontChanged.add(font => { |
65 if (Plugin.plugin.viewFont != null) |
42 if (Plugin.plugin.viewFont != null) |
66 fontResolver.setFontMapping("Isabelle", Plugin.plugin.viewFont) |
43 fontResolver.setFontMapping("Isabelle", Plugin.plugin.viewFont) |
67 |
|
68 panel.relayout() |
44 panel.relayout() |
69 }) |
45 }) |
70 |
|
71 panel.setDocument(message, UserAgent.baseURL) |
46 panel.setDocument(message, UserAgent.baseURL) |
72 panel |
47 panel |
73 } |
48 } |
74 |
49 |
75 // panel has to be displayable in container message_view for doLayout to have |
50 def relayout_panel (panel: XHTMLPanel, width : Int) { |
76 // an effect, especially returning correct preferredSize |
51 // ATTENTION: |
77 def calculate_preferred_size(panel: XHTMLPanel){ |
52 // panel has to be displayable in a frame/container message_view for doDocumentLayout to have |
78 message_panel.add (panel) |
53 // an effect, especially returning correct preferredSize |
79 panel.setBounds (0, 0, message_panel.getWidth, 1) // document has to fit into width |
54 panel.setBounds (0, 0, width, 1) // document has to fit into width |
80 panel.doDocumentLayout (panel.getGraphics) //lay out, preferred size is set then |
55 panel.doDocumentLayout (panel.getGraphics) //lay out, preferred size is set then |
81 // if there are thousands of empty panels, all have to be rendered - |
56 // if there are thousands of empty panels, all have to be rendered - |
82 // but this takes time (even for empty panels); therefore minimum size |
57 // but this takes time (even for empty panels); therefore minimum size |
83 panel.setPreferredSize(new java.awt.Dimension(message_panel.getWidth,Math.max(50, panel.getPreferredSize.getHeight.toInt))) |
58 panel.setPreferredSize(new java.awt.Dimension(width,Math.max(25, panel.getPreferredSize.getHeight.toInt))) |
84 } |
59 } |
85 |
60 |
86 //render and load a message into cache, place its bottom at y-coordinate; |
61 } |
87 def set_message (message_no: Int, y: Int) = { |
62 |
|
63 class MessagePanel(cache: Rendered[XHTMLPanel]) extends JPanel { |
|
64 // defining the current view |
|
65 var offset = 0 //how many pixels of the lowest message are hidden |
|
66 var no = -1 //index of the lowest message |
|
67 |
|
68 // preferences |
|
69 val scrollunit = 5 |
|
70 setLayout(null) |
|
71 |
|
72 // place the bottom of the message at y-coordinate and return the rendered panel |
|
73 def place_message (message_no: Int, y: Int) = { |
88 //add panel to cache if necessary and possible |
74 //add panel to cache if necessary and possible |
89 if(!message_cache.isDefinedAt(message_no) && message_buffer.isDefinedAt(message_no)){ |
75 cache.getRendered(message_no) match { |
90 message_cache = message_cache.update (message_no, render (message_buffer(message_no))) |
|
91 } |
|
92 val result = message_cache.get(message_no) match { |
|
93 case Some(panel) => { |
76 case Some(panel) => { |
|
77 //panel has to be displayable before calculating preferred size! |
|
78 add(panel) |
94 // recalculate preferred size after resizing the message_view |
79 // recalculate preferred size after resizing the message_view |
95 if(panel.getPreferredSize.getWidth.toInt != message_panel.getWidth) |
80 if(panel.getPreferredSize.getWidth.toInt != getWidth){ |
96 calculate_preferred_size (panel) |
81 Renderer.relayout_panel (panel, getWidth) |
97 //place message on view |
82 } |
98 val width = panel.getPreferredSize.getWidth.toInt |
83 val width = panel.getPreferredSize.getWidth.toInt |
99 val height = panel.getPreferredSize.getHeight.toInt |
84 val height = panel.getPreferredSize.getHeight.toInt |
100 panel.setBounds(0, y - height, width, height) |
85 panel.setBounds(0, y - height, width, height) |
101 message_panel.add(panel) |
|
102 panel |
86 panel |
103 } |
87 } |
104 case None => null |
88 case None => null |
105 } |
89 } |
106 result |
|
107 } |
90 } |
108 |
91 |
109 //move view a given amount of pixels |
92 //move view a given amount of pixels |
110 // attention: y should be small, because messages are rendered incremental |
93 // attention: y should be small, because messages are rendered incremental |
111 // (no knowledge on height of messages) |
94 // (no knowledge on height of messages) |
112 def move_view (y : Int) = { |
95 def move_view (y : Int) = { |
113 var update = false |
96 var update = false |
114 if(message_panel.getComponentCount >= 1){ |
97 if(getComponentCount >= 1){ |
115 message_offset += y |
98 offset += y |
116 //remove bottommost panels |
99 //remove bottommost panels |
117 while (message_offset >= message_panel.getComponent(0).getHeight) |
100 while (offset >= getComponent(0).getHeight) |
118 { |
101 { |
119 message_offset -= message_panel.getComponent(0).getHeight |
102 offset -= getComponent(0).getHeight |
120 message_no -= 1 |
103 no -= 1 |
121 update_view |
104 invalidate |
122 update = true |
105 update = true |
123 } |
106 } |
124 //insert panels at the bottom |
107 //insert panels at the bottom |
125 while (message_offset < 0) { |
108 while (offset < 0) { |
126 message_no += 1 |
109 no += 1 |
127 val panel = set_message (message_no, 0) |
110 val panel = place_message (no, 0) |
128 message_offset += panel.getHeight |
111 offset += panel.getHeight |
129 update_view |
112 invalidate |
130 update = true |
113 update = true |
131 } |
114 } |
132 //insert panel at the top |
115 //insert panel at the top |
133 if (message_panel.getComponent(message_panel.getComponentCount - 1).getY + y > 0){ |
116 if (getComponent(getComponentCount - 1).getY + y > 0){ |
134 update_view |
117 invalidate |
135 update = true |
118 update = true |
136 } |
119 } |
137 //simply move panels |
120 //simply move panels |
138 if(!update){ |
121 if(!update){ |
139 message_panel.getComponents map (c => { |
122 getComponents map (c => { |
140 val newrect = c.getBounds |
123 val newrect = c.getBounds |
141 newrect.y = newrect.y + y |
124 newrect.y = newrect.y + y |
142 c.setBounds(newrect) |
125 c.setBounds(newrect) |
143 }) |
126 }) |
144 repaint() |
127 repaint() |
145 } else { |
128 } else { |
146 vscroll.setValue(message_no) |
129 //vscroll.setValue(no) |
147 } |
130 //TODO: create method to update vscroll |
148 } |
131 System.err.println("lookatme2") |
149 } |
132 } |
150 |
133 } |
151 def update_view = { |
134 } |
152 message_panel.removeAll() |
135 |
153 var n = message_no |
136 override def doLayout = { |
154 var y:Int = message_panel.getHeight + message_offset |
137 removeAll() |
|
138 var n = no |
|
139 var y:Int = getHeight + offset |
155 while (y >= 0 && n >= 0){ |
140 while (y >= 0 && n >= 0){ |
156 val panel = set_message (n, y) |
141 val panel = place_message (n, y) |
157 panel.setBorder(javax.swing.border.LineBorder.createBlackLineBorder) |
142 panel.setBorder(javax.swing.border.LineBorder.createBlackLineBorder) |
158 y = y - panel.getHeight |
143 y = y - panel.getHeight |
159 n = n - 1 |
144 n = n - 1 |
160 } |
145 } |
161 repaint() |
146 } |
162 } |
147 } |
163 |
148 |
|
149 class InfoPanel extends JPanel { |
|
150 val auto_scroll = new JRadioButton("Auto Scroll", false) |
|
151 val message_ind = new JTextArea("0") |
|
152 add(message_ind) |
|
153 add(auto_scroll) |
|
154 |
|
155 def isAutoScroll = auto_scroll.isSelected |
|
156 def setIndicator(b: Boolean) = { |
|
157 message_ind.setBackground(if (b) java.awt.Color.red else java.awt.Color.white) |
|
158 } |
|
159 def setText(s: String) { |
|
160 message_ind.setText(s) |
|
161 } |
|
162 |
|
163 } |
|
164 |
|
165 class ScrollerDockable(view : View, position : String) extends JPanel with AdjustmentListener { |
|
166 |
|
167 val buffer:Unrendered[Document] = new MessageBuffer() |
|
168 val cache:Rendered[XHTMLPanel] = new PanelCache(buffer) |
|
169 |
|
170 // set up view |
|
171 val message_panel = new MessagePanel(cache) |
|
172 val infopanel = new InfoPanel |
|
173 val vscroll = new JScrollBar(Adjustable.VERTICAL, 0, 1, 0, 1) |
|
174 vscroll.addAdjustmentListener(this) |
|
175 |
|
176 setLayout(new BorderLayout()) |
|
177 add (vscroll, BorderLayout.EAST) |
|
178 add (message_panel, BorderLayout.CENTER) |
|
179 add (infopanel, BorderLayout.SOUTH) |
|
180 |
164 // do not show every new message, wait a certain amount of time |
181 // do not show every new message, wait a certain amount of time |
165 val message_ind_timer : Timer = new Timer(250, new ActionListener { |
182 val message_ind_timer : Timer = new Timer(250, new ActionListener { |
166 // this method is called to indicate a new message |
183 // this method is called to indicate a new message |
167 override def actionPerformed(e:ActionEvent){ |
184 override def actionPerformed(e:ActionEvent){ |
168 //fire scroll-event if necessary and wanted |
185 //fire scroll-event if necessary and wanted |
169 if(message_no != message_buffer.size |
186 if(message_panel.no != buffer.size && infopanel.isAutoScroll) { |
170 && auto_scroll.isSelected) { |
187 vscroll.setValue(buffer.size - 1) |
171 vscroll.setValue(message_buffer.size - 1) |
|
172 } |
188 } |
173 message_ind.setBackground(java.awt.Color.white) |
189 infopanel.setIndicator(false) |
174 } |
190 } |
175 }) |
191 }) |
176 |
192 |
177 def add_message (message: Document) = { |
193 def add_message (message: Document) = { |
178 message_buffer += message |
194 buffer.addUnrendered(buffer.size, message) |
179 vscroll.setMaximum (Math.max(1, message_buffer.size)) |
195 vscroll.setMaximum (Math.max(1, buffer.size)) |
180 message_ind.setBackground(java.awt.Color.red) |
196 infopanel.setIndicator(true) |
181 message_ind.setText(message_buffer.size.toString) |
197 infopanel.setText(buffer.size.toString) |
|
198 |
182 if (! message_ind_timer.isRunning()){ |
199 if (! message_ind_timer.isRunning()){ |
183 if(auto_scroll.isSelected) vscroll.setValue(message_buffer.size - 1) |
200 if(infopanel.isAutoScroll){ |
|
201 vscroll.setValue(buffer.size - 1) |
|
202 } |
184 message_ind_timer.setRepeats(false) |
203 message_ind_timer.setRepeats(false) |
185 message_ind_timer.start() |
204 message_ind_timer.start() |
186 } |
205 } |
187 } |
206 |
188 |
207 if(message_panel.no == -1) { |
|
208 message_panel.no = 0 |
|
209 message_panel.invalidate |
|
210 } |
|
211 } |
|
212 |
189 override def adjustmentValueChanged (e : AdjustmentEvent) = { |
213 override def adjustmentValueChanged (e : AdjustmentEvent) = { |
190 //event-handling has to be so general (without UNIT_INCR,...) |
214 //event-handling has to be so general (without UNIT_INCR,...) |
191 // because all events could be sent as TRACK e.g. on my mac |
215 // because all events could be sent as TRACK e.g. on my mac |
192 if (e.getSource == vscroll){ |
216 if (e.getSource == vscroll){ |
193 message_no = e.getValue |
217 message_panel.no = e.getValue |
194 message_offset = 0 |
218 message_panel.offset = 0 |
195 update_view |
219 message_panel.invalidate |
196 } |
220 System.err.println("event: "+message_panel.no) |
197 } |
221 vscroll.setModel(new javax.swing.DefaultBoundedRangeModel(99,1,0,1000)) |
198 |
222 System.err.println("hello"+e.getValue) |
199 // handle Component-Events |
223 } |
200 override def componentShown(e: ComponentEvent){} |
224 } |
201 override def componentHidden(e: ComponentEvent){} |
225 |
202 override def componentMoved(e: ComponentEvent){} |
|
203 override def componentResized(e: ComponentEvent){ |
|
204 update_view |
|
205 } |
|
206 |
226 |
207 // TODO: register somewhere |
227 // TODO: register somewhere |
208 // here only 'emulation of message stream' |
228 // here only 'emulation of message stream' |
209 Plugin.plugin.stateUpdate.add(state => { |
229 Plugin.plugin.stateUpdate.add(state => { |
210 var i = 0 |
230 var i = 0 |
211 if(state != null) new Thread{ |
231 if(state != null) new Thread{ |
212 override def run() { |
232 override def run() { |
213 while (i < 500) { |
233 while (i < 1) { |
214 add_message(state.document) |
234 add_message(state.document) |
215 i += 1 |
235 i += 1 |
216 try {Thread.sleep(3)} |
236 try {Thread.sleep(3)} |
217 catch{case _ =>} |
237 catch{case _ =>} |
218 } |
238 } |