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