22
22
import java .awt .BorderLayout ;
23
23
import java .awt .Component ;
24
24
import java .awt .event .KeyEvent ;
25
+ import java .awt .event .MouseEvent ;
25
26
import java .util .ArrayList ;
26
27
import java .util .Comparator ;
27
28
import java .util .HashMap ;
34
35
import javax .swing .JPanel ;
35
36
import javax .swing .JScrollPane ;
36
37
import javax .swing .JToolBar ;
38
+ import javax .swing .event .ChangeListener ;
39
+ import javax .swing .event .DocumentEvent ;
40
+ import javax .swing .event .DocumentListener ;
37
41
import javax .swing .text .DefaultCaret ;
38
42
import org .apache .commons .lang3 .exception .ExceptionUtils ;
39
43
import org .parosproxy .paros .Constant ;
47
51
import org .zaproxy .zap .utils .TimeStampUtils ;
48
52
import org .zaproxy .zap .utils .ZapTextArea ;
49
53
import org .zaproxy .zap .view .OutputSource ;
54
+ import org .zaproxy .zap .view .OverlayIcon ;
50
55
import org .zaproxy .zap .view .TabbedPanel2 ;
51
56
import org .zaproxy .zap .view .ZapToggleButton ;
52
57
@@ -77,15 +82,19 @@ public class TabbedOutputPanel extends OutputPanel {
77
82
getImageIcon ("/org/zaproxy/addon/commonlib/resources/ui-scroll-pane.png" );
78
83
private static final ImageIcon SCROLL_LOCK_ENABLED_ICON =
79
84
getImageIcon ("/org/zaproxy/addon/commonlib/resources/ui-scroll-lock-pane.png" );
85
+ private static final ImageIcon GREEN_BADGE_CORNER_ICON =
86
+ getImageIcon ("/org/zaproxy/addon/commonlib/resources/green-badge-corner.png" );
80
87
81
88
private final TabbedPanel2 tabbedPanel ;
82
89
83
90
private final Map <String , ZapTextArea > txtOutputs ;
84
91
private final Map <String , OutputSource > registeredOutputSources ;
92
+ private final Map <String , ChangeListener > outputSourceChangeListeners ;
85
93
86
94
public TabbedOutputPanel () {
87
95
txtOutputs = new HashMap <>();
88
96
registeredOutputSources = new HashMap <>();
97
+ outputSourceChangeListeners = new HashMap <>();
89
98
90
99
setLayout (new BorderLayout ());
91
100
setName (Constant .messages .getString ("commonlib.output.panel.title" ));
@@ -124,6 +133,10 @@ public void unregisterOutputSource(OutputSource source) {
124
133
txtOutputs .remove (source .getName ());
125
134
}
126
135
registeredOutputSources .remove (source .getName ());
136
+ ChangeListener listener = outputSourceChangeListeners .remove (source .getName ());
137
+ if (listener != null ) {
138
+ tabbedPanel .removeChangeListener (listener );
139
+ }
127
140
}
128
141
129
142
private void addNewOutputSource (String name ) {
@@ -138,12 +151,24 @@ private void addNewOutputSource(String name) {
138
151
var outputPanel = new AbstractPanel ();
139
152
outputPanel .setName (name );
140
153
outputPanel .setLayout (new BorderLayout ());
141
- if (attributes .containsKey (ATTRIBUTE_ICON )
142
- && attributes .get (ATTRIBUTE_ICON ) instanceof Icon ) {
143
- outputPanel .setIcon ((Icon ) attributes .get (ATTRIBUTE_ICON ));
144
- }
154
+ Icon icon =
155
+ attributes .containsKey (ATTRIBUTE_ICON )
156
+ && attributes .get (ATTRIBUTE_ICON ) instanceof Icon
157
+ ? (Icon ) attributes .get (ATTRIBUTE_ICON )
158
+ : DOC_ICON ;
159
+ outputPanel .setIcon (icon );
160
+
161
+ var changeListener =
162
+ new ChangeListener () {
163
+ @ Override
164
+ public void stateChanged (javax .swing .event .ChangeEvent e ) {
165
+ markTabRead (outputPanel , icon );
166
+ }
167
+ };
168
+ tabbedPanel .addChangeListener (changeListener );
169
+ outputSourceChangeListeners .put (name , changeListener );
145
170
146
- ZapTextArea txtOutput = buildOutputTextArea ();
171
+ ZapTextArea txtOutput = buildOutputTextArea (outputPanel , icon );
147
172
JToolBar toolBar = buildToolbar (txtOutput , attributes );
148
173
outputPanel .add (toolBar , BorderLayout .PAGE_START );
149
174
var jScrollPane = new JScrollPane ();
@@ -157,7 +182,7 @@ private void addNewOutputSource(String name) {
157
182
txtOutputs .put (name , txtOutput );
158
183
}
159
184
160
- private static ZapTextArea buildOutputTextArea () {
185
+ private ZapTextArea buildOutputTextArea (AbstractPanel outputPanel , Icon icon ) {
161
186
var txtOutput = new ZapTextArea ();
162
187
txtOutput .setEditable (false );
163
188
txtOutput .setLineWrap (true );
@@ -174,6 +199,11 @@ public void mouseReleased(java.awt.event.MouseEvent e) {
174
199
showPopupMenuIfTriggered (e );
175
200
}
176
201
202
+ @ Override
203
+ public void mouseEntered (MouseEvent e ) {
204
+ markTabRead (outputPanel , icon );
205
+ }
206
+
177
207
private void showPopupMenuIfTriggered (java .awt .event .MouseEvent e ) {
178
208
if (e .isPopupTrigger ()) {
179
209
View .getSingleton ()
@@ -182,9 +212,49 @@ private void showPopupMenuIfTriggered(java.awt.event.MouseEvent e) {
182
212
}
183
213
}
184
214
});
215
+
216
+ // Mark tab unread with a green dot when there's new text
217
+ // Note that OverlayIcon only supports ImageIcons so this will not work for regular Icons
218
+ // However, most (all?) icons in ZAP are ImageIcons so we should probably be fine
219
+ if (icon instanceof ImageIcon imageIcon ) {
220
+ var overlayIcon = new OverlayIcon (imageIcon );
221
+ overlayIcon .add (GREEN_BADGE_CORNER_ICON );
222
+ txtOutput
223
+ .getDocument ()
224
+ .addDocumentListener (
225
+ new DocumentListener () {
226
+ @ Override
227
+ public void insertUpdate (DocumentEvent e ) {
228
+ if (outputPanel .getIcon () != overlayIcon ) {
229
+ setTabIcon (outputPanel , overlayIcon );
230
+ }
231
+ }
232
+
233
+ @ Override
234
+ public void removeUpdate (DocumentEvent e ) {}
235
+
236
+ @ Override
237
+ public void changedUpdate (DocumentEvent e ) {}
238
+ });
239
+ }
240
+
185
241
return txtOutput ;
186
242
}
187
243
244
+ private void markTabRead (AbstractPanel outputPanel , Icon originalIcon ) {
245
+ if (outputPanel .isShowing () && outputPanel .equals (tabbedPanel .getSelectedComponent ())) {
246
+ setTabIcon (outputPanel , originalIcon );
247
+ }
248
+ }
249
+
250
+ private void setTabIcon (AbstractPanel outputPanel , Icon icon ) {
251
+ outputPanel .setIcon (icon );
252
+ int index = tabbedPanel .indexOfComponent (outputPanel );
253
+ if (index != -1 ) {
254
+ tabbedPanel .setIconAt (index , icon );
255
+ }
256
+ }
257
+
188
258
private static JToolBar buildToolbar (ZapTextArea txtOutput , Map <String , Object > attributes ) {
189
259
List <AbstractButton > buttons = new ArrayList <>();
190
260
@@ -269,6 +339,8 @@ public void appendAsync(String message, String sourceName) {
269
339
270
340
@ Override
271
341
public void clear () {
342
+ outputSourceChangeListeners .values ().forEach (tabbedPanel ::removeChangeListener );
343
+ outputSourceChangeListeners .clear ();
272
344
tabbedPanel .removeAll ();
273
345
txtOutputs .clear ();
274
346
addNewOutputSource (DEFAULT_OUTPUT_SOURCE_NAME );
0 commit comments