don't use Color.BLUE
[idea/community.git] / plugins / ui-designer / src / com / intellij / uiDesigner / designSurface / Painter.java
1 /*
2  * Copyright 2000-2012 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.uiDesigner.designSurface;
17
18 import com.intellij.ui.LightColors;
19 import com.intellij.uiDesigner.FormEditingUtil;
20 import com.intellij.uiDesigner.SwingProperties;
21 import com.intellij.uiDesigner.core.GridLayoutManager;
22 import com.intellij.uiDesigner.lw.IProperty;
23 import com.intellij.uiDesigner.lw.StringDescriptor;
24 import com.intellij.uiDesigner.radComponents.*;
25 import com.intellij.uiDesigner.shared.BorderType;
26 import com.intellij.util.ui.PlatformColors;
27 import org.intellij.lang.annotations.JdkConstants;
28 import org.jetbrains.annotations.NotNull;
29
30 import javax.swing.*;
31 import java.awt.*;
32 import java.awt.geom.Rectangle2D;
33 import java.util.ArrayList;
34 import java.util.List;
35
36 /**
37  * @author Anton Katilin
38  * @author Vladimir Kondratyev
39  */
40 public final class Painter {
41   /**
42    * This color is used to paint decoration of non selected components
43    */
44   private static final Color NON_SELECTED_BOUNDARY_COLOR = new Color(114, 126, 143);
45   /**
46    * This color is used to paint decoration of selected components
47    */
48   private static final Color SELECTED_BOUNDARY_COLOR = new Color(8, 8, 108);
49
50   private static final Color HIGHLIGHTED_BOUNDARY_COLOR = Color.RED;
51
52   /**
53    * This color is used to paint grid cell for selected container
54    */
55   static final Color SELECTED_GRID_COLOR = new Color(47, 67, 96);
56   /**
57    * This color is used to paint grid cell for non selected container
58    */
59   static final Color NON_SELECTED_GRID_COLOR = new Color(130, 140, 155);
60
61   public final static int WEST_MASK = 1;
62   public final static int EAST_MASK = 2;
63   public final static int NORTH_MASK = 4;
64   public final static int SOUTH_MASK = 8;
65   private final static int R = 2;
66   private final static int GAP = R;
67   private static final int NW = 0;
68   private static final int N = 1;
69   private static final int NE = 2;
70   private static final int E = 3;
71   private static final int SE = 4;
72   private static final int S = 5;
73   private static final int SW = 6;
74   private static final int W = 7;
75
76   private Painter() {
77   }
78
79   public static void paintComponentDecoration(final GuiEditor editor, final RadComponent component, final Graphics g) {
80     // Collect selected components and paint decoration for non selected components
81     final ArrayList<RadComponent> selection = new ArrayList<RadComponent>();
82     final Rectangle layeredPaneRect = editor.getLayeredPane().getVisibleRect();
83     FormEditingUtil.iterate(
84       component,
85       new FormEditingUtil.ComponentVisitor<RadComponent>() {
86         public boolean visit(final RadComponent component) {
87           if (!component.getDelegee().isShowing()) { // Skip invisible components
88             return true;
89           }
90           final Shape oldClip = g.getClip();
91           final RadContainer parent = component.getParent();
92           if (parent != null) {
93             final Point p = SwingUtilities.convertPoint(component.getDelegee(), 0, 0, editor.getLayeredPane());
94             final Rectangle visibleRect = layeredPaneRect.intersection(new Rectangle(p.x, p.y, parent.getWidth(), parent.getHeight()));
95             g.setClip(visibleRect);
96           }
97           if (component.isSelected()) { // we will paint selection later
98             selection.add(component);
99           }
100           else {
101             paintComponentBoundsImpl(editor, component, g);
102           }
103           paintGridOutline(editor, component, g);
104           if (parent != null) {
105             g.setClip(oldClip);
106           }
107           return true;
108         }
109       }
110     );
111
112     // Let's paint decoration for selected components
113     for (int i = selection.size() - 1; i >= 0; i--) {
114       final Shape oldClip = g.getClip();
115       final RadComponent c = selection.get(i);
116       final RadContainer parent = c.getParent();
117       if (parent != null) {
118         final Point p = SwingUtilities.convertPoint(c.getDelegee(), 0, 0, editor.getLayeredPane());
119         final Rectangle visibleRect = layeredPaneRect.intersection(new Rectangle(p.x, p.y, parent.getWidth(), parent.getHeight()));
120         g.setClip(visibleRect);
121       }
122       paintComponentBoundsImpl(editor, c, g);
123       if (parent != null) {
124         g.setClip(oldClip);
125       }
126     }
127   }
128
129   /**
130    * Paints container border. For grids the method also paints vertical and
131    * horizontal lines that indicate bounds of the rows and columns.
132    * Method does nothing if the <code>component</code> is not an instance
133    * of <code>RadContainer</code>.
134    */
135   private static void paintComponentBoundsImpl(final GuiEditor editor, @NotNull final RadComponent component, final Graphics g) {
136     if (!(component instanceof RadContainer) && !(component instanceof RadNestedForm) && !component.isDragBorder()) {
137       return;
138     }
139
140     boolean highlightBoundaries = (getDesignTimeInsets(component) > 2);
141
142     if (component instanceof RadContainer && !component.isDragBorder()) {
143       RadContainer container = (RadContainer)component;
144       if (!highlightBoundaries && (container.getBorderTitle() != null || container.getBorderType() != BorderType.NONE)) {
145         return;
146       }
147     }
148     final Point point = SwingUtilities.convertPoint(
149       component.getDelegee(),
150       0,
151       0,
152       editor.getRootContainer().getDelegee()
153     );
154     g.translate(point.x, point.y);
155     try {
156       if (component.isDragBorder()) {
157         Graphics2D g2d = (Graphics2D)g;
158         g2d.setColor(LightColors.YELLOW);
159         g2d.setStroke(new BasicStroke(2.0f));
160         g2d.translate(1, 1);
161       }
162       else if (highlightBoundaries) {
163         g.setColor(HIGHLIGHTED_BOUNDARY_COLOR);
164       }
165       else if (component.isSelected()) {
166         g.setColor(SELECTED_BOUNDARY_COLOR);
167       }
168       else {
169         g.setColor(NON_SELECTED_BOUNDARY_COLOR);
170       }
171       g.drawRect(0, 0, component.getWidth() - 1, component.getHeight() - 1);
172       if (component.isDragBorder()) {
173         g.translate(-1, -1);
174       }
175     }
176     finally {
177       g.translate(-point.x, -point.y);
178     }
179   }
180
181   private static int getDesignTimeInsets(RadComponent component) {
182     while (component != null) {
183       Integer designTimeInsets = (Integer)component.getDelegee().getClientProperty(GridLayoutManager.DESIGN_TIME_INSETS);
184       if (designTimeInsets != null) {
185         return designTimeInsets.intValue();
186       }
187       component = component.getParent();
188     }
189     return 0;
190   }
191
192   /**
193    * This method paints grid bounds for "grid" containers
194    */
195   public static void paintGridOutline(final GuiEditor editor, @NotNull final RadComponent component, final Graphics g) {
196     if (!editor.isShowGrid()) {
197       return;
198     }
199     if (!(component instanceof RadContainer)) {
200       return;
201     }
202     final RadContainer container = (RadContainer)component;
203     if (!container.getLayoutManager().isGrid()) {
204       return;
205     }
206
207     // performance: don't paint grid outline in drag layer
208     Container parent = component.getDelegee().getParent();
209     while (parent != null) {
210       if (parent == editor.getDragLayer()) {
211         return;
212       }
213       parent = parent.getParent();
214     }
215
216     final Point point = SwingUtilities.convertPoint(
217       component.getDelegee(),
218       0,
219       0,
220       editor.getRootContainer().getDelegee()
221     );
222     g.translate(point.x, point.y);
223     try {
224       // Paint grid
225       if (container.getWidth() > 0 && container.getHeight() > 0) {
226         Image gridImage = CachedGridImage.getGridImage(container);
227         g.drawImage(gridImage, 0, 0, null);
228       }
229     }
230     finally {
231       g.translate(-point.x, -point.y);
232     }
233   }
234
235   /**
236    * Paints selection for the specified <code>component</code>.
237    */
238   public static void paintSelectionDecoration(@NotNull RadComponent component, Graphics g,
239                                               boolean focused) {
240     if (component.isSelected()) {
241       if (focused) {
242         g.setColor(PlatformColors.BLUE);
243       }
244       else {
245         g.setColor(Color.GRAY);
246       }
247       final Point[] points = getPoints(component.getWidth(), component.getHeight());
248       for (final Point point : points) {
249         g.fillRect(point.x - R, point.y - R, 2 * R + 1, 2 * R + 1);
250       }
251     }
252     else if (component.getWidth() < FormEditingUtil.EMPTY_COMPONENT_SIZE || component.getHeight() < FormEditingUtil.EMPTY_COMPONENT_SIZE) {
253       Graphics2D g2d = (Graphics2D)g;
254       Composite oldComposite = g2d.getComposite();
255       Stroke oldStroke = g2d.getStroke();
256       Color oldColor = g2d.getColor();
257
258       g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.5f));
259       g2d.setStroke(new BasicStroke(0.7f));
260       g2d.setColor(Color.black);
261       g2d.drawRect(0, 0, Math.max(component.getWidth(), FormEditingUtil.EMPTY_COMPONENT_SIZE),
262                    Math.max(component.getHeight(), FormEditingUtil.EMPTY_COMPONENT_SIZE));
263
264       g2d.setComposite(oldComposite);
265       g2d.setStroke(oldStroke);
266       g2d.setColor(oldColor);
267     }
268   }
269
270   /**
271    * @param x in component's coord system
272    * @param y in component's coord system
273    */
274   public static int getResizeMask(@NotNull final RadComponent component, final int x, final int y) {
275     if (component.getParent() == null || !component.isSelected()) {
276       return 0;
277     }
278
279     // only components in XY can be resized...
280     /*
281     if (!component.getParent().isXY()) {
282       return 0;
283     }
284     */
285
286     final int width = component.getWidth();
287     final int height = component.getHeight();
288
289     final Point[] points = getPoints(width, height);
290
291     if (isInside(x, y, points[SE])) {
292       return EAST_MASK | SOUTH_MASK;
293     }
294     else if (isInside(x, y, points[NW])) {
295       return WEST_MASK | NORTH_MASK;
296     }
297     else if (isInside(x, y, points[N])) {
298       return NORTH_MASK;
299     }
300     else if (isInside(x, y, points[NE])) {
301       return EAST_MASK | NORTH_MASK;
302     }
303     else if (isInside(x, y, points[W])) {
304       return WEST_MASK;
305     }
306     else if (isInside(x, y, points[E])) {
307       return EAST_MASK;
308     }
309     else if (isInside(x, y, points[SW])) {
310       return WEST_MASK | SOUTH_MASK;
311     }
312     else if (isInside(x, y, points[S])) {
313       return SOUTH_MASK;
314     }
315     else {
316       return 0;
317     }
318   }
319
320   private static boolean isInside(final int x, final int y, final Point r) {
321     return x >= r.x - R && x <= r.x + R && y >= r.y - R && y <= r.y + R;
322   }
323
324   @JdkConstants.CursorType
325   public static int getResizeCursor(final int resizeMask) {
326     if (resizeMask == (WEST_MASK | NORTH_MASK)) {
327       return Cursor.NW_RESIZE_CURSOR;
328     }
329     else if (resizeMask == NORTH_MASK) {
330       return Cursor.N_RESIZE_CURSOR;
331     }
332     else if (resizeMask == (EAST_MASK | NORTH_MASK)) {
333       return Cursor.NE_RESIZE_CURSOR;
334     }
335     else if (resizeMask == WEST_MASK) {
336       return Cursor.W_RESIZE_CURSOR;
337     }
338     else if (resizeMask == EAST_MASK) {
339       return Cursor.E_RESIZE_CURSOR;
340     }
341     else if (resizeMask == (WEST_MASK | SOUTH_MASK)) {
342       return Cursor.SW_RESIZE_CURSOR;
343     }
344     else if (resizeMask == SOUTH_MASK) {
345       return Cursor.S_RESIZE_CURSOR;
346     }
347     else if (resizeMask == (EAST_MASK | SOUTH_MASK)) {
348       return Cursor.SE_RESIZE_CURSOR;
349     }
350     else {
351       throw new IllegalArgumentException("unknown resizeMask: " + resizeMask);
352     }
353   }
354
355   public static Point[] getPoints(final int width, final int height) {
356     final Point[] points = new Point[8];
357
358     points[NW] = new Point(GAP, GAP); // NW
359     points[N] = new Point(width / 2, GAP); // N
360     points[NE] = new Point(width - GAP - 1, GAP); // NE
361     points[E] = new Point(width - GAP - 1, height / 2); // E
362     points[SE] = new Point(width - GAP - 1, height - GAP - 1); // SE
363     points[S] = new Point(width / 2, height - GAP - 1); // S
364     points[SW] = new Point(GAP, height - GAP - 1); // SW
365     points[W] = new Point(GAP, height / 2); // W
366
367     return points;
368   }
369
370   public static void paintButtonGroupLines(RadRootContainer rootContainer, RadButtonGroup group, Graphics g) {
371     List<RadComponent> components = rootContainer.getGroupContents(group);
372     if (components.size() < 2) return;
373     Rectangle[] allBounds = new Rectangle[components.size()];
374     int lastTop = -1;
375     int minLeft = Integer.MAX_VALUE;
376     for (int i = 0; i < components.size(); i++) {
377       final Rectangle rc = SwingUtilities.convertRectangle(
378         components.get(i).getParent().getDelegee(),
379         components.get(i).getBounds(),
380         rootContainer.getDelegee()
381       );
382       allBounds[i] = rc;
383
384       minLeft = Math.min(minLeft, rc.x);
385       if (i == 0) {
386         lastTop = rc.y;
387       }
388       else if (lastTop != rc.y) {
389         lastTop = Integer.MIN_VALUE;
390       }
391     }
392
393     Graphics2D g2d = (Graphics2D)g;
394     Stroke oldStroke = g2d.getStroke();
395     g2d.setStroke(new BasicStroke(2.0f));
396     g2d.setColor(new Color(104, 107, 130));
397     if (lastTop != Integer.MIN_VALUE) {
398       // all items in group have same Y
399       int left = Integer.MAX_VALUE;
400       int right = Integer.MIN_VALUE;
401       for (Rectangle rc : allBounds) {
402         final int midX = (int)rc.getCenterX();
403         left = Math.min(left, midX);
404         right = Math.max(right, midX);
405         g2d.drawLine(midX, lastTop - 8, midX, lastTop);
406       }
407       g2d.drawLine(left, lastTop - 8, right, lastTop - 8);
408     }
409     else {
410       int top = Integer.MAX_VALUE;
411       int bottom = Integer.MIN_VALUE;
412       for (Rectangle rc : allBounds) {
413         final int midY = (int)rc.getCenterY();
414         top = Math.min(top, midY);
415         bottom = Math.max(bottom, midY);
416         g2d.drawLine(minLeft - 8, midY, rc.x, midY);
417       }
418       g2d.drawLine(minLeft - 8, top, minLeft - 8, bottom);
419     }
420     g2d.setStroke(oldStroke);
421   }
422
423   public static void paintComponentTag(final RadComponent component, final Graphics g) {
424     if (component instanceof RadContainer) return;
425     for (IProperty prop : component.getModifiedProperties()) {
426       if (prop.getName().equals(SwingProperties.TEXT)) {
427         final Object desc = prop.getPropertyValue(component);
428         if (!(desc instanceof StringDescriptor) || ((StringDescriptor)desc).getValue() == null ||
429             ((StringDescriptor)desc).getValue().length() > 0) {
430           return;
431         }
432       }
433       else if (prop.getName().equals(SwingProperties.MODEL)) {
434         // don't paint tags on non-empty lists
435         final Object value = prop.getPropertyValue(component);
436         if (value instanceof String[] && ((String[])value).length > 0) {
437           return;
438         }
439       }
440     }
441
442     Rectangle bounds = component.getDelegee().getBounds();
443     if (bounds.width > 100 && bounds.height > 40) {
444       StringBuilder tagBuilder = new StringBuilder();
445       if (component.getBinding() != null) {
446         tagBuilder.append(component.getBinding()).append(':');
447       }
448       String className = component.getComponentClassName();
449       int pos = className.lastIndexOf('.');
450       if (pos >= 0) {
451         tagBuilder.append(className.substring(pos + 1));
452       }
453       else {
454         tagBuilder.append(className);
455       }
456       final Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(tagBuilder.toString(), g);
457       Graphics2D g2d = (Graphics2D)g;
458       g2d.setColor(PlatformColors.BLUE);
459       g2d.fillRect(0, 0, (int)stringBounds.getWidth(), (int)stringBounds.getHeight());
460       g2d.setColor(Color.WHITE);
461       g.drawString(tagBuilder.toString(), 0, g.getFontMetrics().getAscent());
462     }
463   }
464 }