001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.util;
020
021import icy.gui.frame.IcyFrame;
022import icy.network.NetworkUtil;
023import icy.system.SystemUtil;
024
025import java.awt.Color;
026import java.awt.Component;
027import java.awt.Container;
028import java.awt.Dimension;
029import java.awt.Font;
030import java.awt.Frame;
031import java.awt.GraphicsDevice;
032import java.awt.Point;
033import java.awt.Rectangle;
034import java.awt.Window;
035import java.awt.geom.Point2D;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.Collections;
039import java.util.List;
040
041import javax.swing.JInternalFrame;
042import javax.swing.JMenu;
043import javax.swing.JSlider;
044import javax.swing.JTextPane;
045import javax.swing.JTree;
046import javax.swing.SwingConstants;
047import javax.swing.SwingUtilities;
048import javax.swing.event.HyperlinkEvent;
049import javax.swing.event.HyperlinkListener;
050import javax.swing.text.MutableAttributeSet;
051import javax.swing.text.StyleConstants;
052import javax.swing.text.StyledDocument;
053import javax.swing.tree.TreeNode;
054import javax.swing.tree.TreePath;
055
056/**
057 * General component utilities class.
058 * 
059 * @author Stephane
060 */
061public class ComponentUtil
062{
063    public static void setPreferredWidth(Component c, int w)
064    {
065        c.setPreferredSize(new Dimension(w, c.getPreferredSize().height));
066    }
067
068    public static void setPreferredHeight(Component c, int h)
069    {
070        c.setPreferredSize(new Dimension(c.getPreferredSize().width, h));
071    }
072
073    public static void setFixedSize(Component c, Dimension d)
074    {
075        c.setMinimumSize(d);
076        c.setMaximumSize(d);
077        c.setPreferredSize(d);
078    }
079
080    public static void setFixedWidth(Component c, int w)
081    {
082        c.setMinimumSize(new Dimension(w, 0));
083        c.setMaximumSize(new Dimension(w, 65535));
084        c.setPreferredSize(new Dimension(w, c.getPreferredSize().height));
085    }
086
087    public static void setFixedHeight(Component c, int h)
088    {
089        c.setMinimumSize(new Dimension(0, h));
090        c.setMaximumSize(new Dimension(65535, h));
091        c.setPreferredSize(new Dimension(c.getPreferredSize().width, h));
092    }
093
094    public static void setPreferredWidth(IcyFrame frm, int w)
095    {
096        frm.setPreferredSize(new Dimension(w, frm.getPreferredSize().height));
097    }
098
099    public static void setPreferredHeight(IcyFrame frm, int h)
100    {
101        frm.setPreferredSize(new Dimension(frm.getPreferredSize().width, h));
102    }
103
104    public static void setFixedSize(IcyFrame frm, Dimension d)
105    {
106        frm.setMinimumSize(d);
107        frm.setMaximumSize(d);
108        frm.setPreferredSize(d);
109    }
110
111    public static void setFixedWidth(IcyFrame frm, int w)
112    {
113        frm.setMinimumSize(new Dimension(w, 0));
114        frm.setMaximumSize(new Dimension(w, 65535));
115        frm.setPreferredSize(new Dimension(w, frm.getPreferredSize().height));
116    }
117
118    public static void setFixedHeight(IcyFrame frm, int h)
119    {
120        frm.setMinimumSize(new Dimension(0, h));
121        frm.setMaximumSize(new Dimension(65535, h));
122        frm.setPreferredSize(new Dimension(frm.getPreferredSize().width, h));
123    }
124
125    public static void removeFixedSize(Component c)
126    {
127        c.setMinimumSize(new Dimension(0, 0));
128        c.setMaximumSize(new Dimension(65535, 65535));
129    }
130
131    /**
132     * Center specified component relative to its parent
133     */
134    public static void center(Component comp)
135    {
136        final Container parent = comp.getParent();
137
138        if (parent != null)
139        {
140            final int x = (parent.getWidth() - comp.getWidth()) / 2;
141            final int y = (parent.getHeight() - comp.getHeight()) / 2;
142
143            // avoid negative coordinates when centering
144            comp.setLocation((x < 0) ? 0 : x, (y < 0) ? 0 : y);
145        }
146    }
147
148    /**
149     * Center specified windows relative to its parent
150     */
151    public static void center(Window window)
152    {
153        window.setLocationRelativeTo(window.getParent());
154    }
155
156    /**
157     * Center specified JInternalFrame
158     */
159    public static void center(JInternalFrame frame)
160    {
161        center((Component) frame);
162    }
163
164    /**
165     * Center the Window on specified point
166     */
167    public static void centerOn(Window window, Point position)
168    {
169        final int x = position.x - (window.getWidth() / 2);
170        final int y = position.y - (window.getHeight() / 2);
171
172        // avoid negative coordinates when centering
173        window.setLocation((x < 0) ? 0 : x, (y < 0) ? 0 : y);
174    }
175
176    /**
177     * Center the JInternalFrame on specified point
178     */
179    public static void centerOn(JInternalFrame f, Point position)
180    {
181        centerOn((Component) f, position);
182    }
183
184    /**
185     * Center specified component relative to its parent
186     */
187    public static void centerOn(Component comp, Point position)
188    {
189        final int x = position.x - (comp.getWidth() / 2);
190        final int y = position.y - (comp.getHeight() / 2);
191
192        // avoid negative coordinates when centering
193        comp.setLocation((x < 0) ? 0 : x, (y < 0) ? 0 : y);
194    }
195
196    /**
197     * Use f.center() instead
198     * 
199     * @deprecated
200     */
201    @Deprecated
202    public static void center(IcyFrame f)
203    {
204        f.center();
205    }
206
207    public static void center(Component dst, Component src)
208    {
209        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
210                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
211    }
212
213    public static void center(IcyFrame dst, Component src)
214    {
215        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
216                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
217    }
218
219    public static void center(Component dst, IcyFrame src)
220    {
221        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
222                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
223    }
224
225    public static void center(IcyFrame dst, IcyFrame src)
226    {
227        dst.setLocation(src.getX() + ((src.getWidth() - dst.getWidth()) / 2),
228                src.getY() + ((src.getHeight() - dst.getHeight()) / 2));
229    }
230
231    /**
232     * Returns the center position of the specified component.
233     */
234    public static Point2D.Double getCenter(Component c)
235    {
236        if (c != null)
237        {
238            final Rectangle r = c.getBounds();
239            return new Point2D.Double(r.getX() + (r.getWidth() / 2d), r.getY() + (r.getHeight() / 2d));
240        }
241
242        return new Point2D.Double(0d, 0d);
243    }
244
245    /**
246     * Returns all screen device where the specified component is currently displayed.<br>
247     * Can return an empty list if given region do not intersect any screen device.
248     * 
249     * @see #getScreen(Component)
250     * @see SystemUtil#getScreenDevices(Rectangle)
251     */
252    public static List<GraphicsDevice> getScreens(Component c)
253    {
254        return SystemUtil.getScreenDevices(c.getBounds());
255    }
256
257    /**
258     * Returns the main screen device where the specified component is currently displayed.<br>
259     * Can return <code>null</code> if component is not located on any screen device.
260     * 
261     * @see #getScreens(Component)
262     * @see SystemUtil#getScreenDevice(Rectangle)
263     * @see SystemUtil#getScreenDevice(Point)
264     */
265    public static GraphicsDevice getScreen(Component c)
266    {
267        final Point2D.Double pos2d = getCenter(c);
268        final Point pos = new Point((int) pos2d.getX(), (int) pos2d.getY());
269
270        // get screen on Component center first (better for multi screen)
271        GraphicsDevice result = SystemUtil.getScreenDevice(pos);
272
273        // cannot retrieve screen on center, just use component bounds then
274        if (result == null)
275            result = SystemUtil.getScreenDevice(c.getBounds());
276
277        return result;
278    }
279
280    /**
281     * Returns the new location of wanted bounds so it does not go outside the specified screen bounds.<br>
282     * Returns <code>null</code> if the wanted bounds doesn't need position adjustment.
283     */
284    public static Point fixPosition(Rectangle wantedBounds, Rectangle screenBounds)
285    {
286        if (screenBounds.isEmpty())
287            return null;
288
289        final int margeX = 80;
290        final int margeY = 40;
291
292        int x = wantedBounds.x;
293        int y = wantedBounds.y;
294        int sx = screenBounds.x;
295        int sy = screenBounds.y;
296        int minX = (sx - wantedBounds.width) + margeX;
297        int maxX = (sx + screenBounds.width) - margeX;
298        int minY = sy;
299//        int minY = (sy - wantedBounds.height) + margeY;
300        int maxY = (sy + screenBounds.height) - margeY;
301
302        if (y < minY)
303            y = minY;
304        else if (y > maxY)
305            y = maxY;
306        if (x < minX)
307            x = minX;
308        else if (x > maxX)
309            x = maxX;
310
311        final Point pos = wantedBounds.getLocation();
312
313        // position changed ?
314        if ((pos.x != x) || (pos.y != y))
315            return new Point(x, y);
316
317        return null;
318    }
319
320    /**
321     * Fix the given bounds of specified component so it does not go completely off screen.<br>
322     * Returns <code>true</code> if the bounds position has be adjusted.
323     */
324    public static boolean fixPosition(Component component, Rectangle wantedBounds)
325    {
326      final List<GraphicsDevice> screens = SystemUtil.getScreenDevices();
327
328      // headless mode probably
329      if (screens.isEmpty())
330          return false;
331
332      Point newPos = null;
333      boolean useMainScreen = false;
334
335      for (GraphicsDevice screen : screens)
336      {
337          final Point pt = fixPosition(wantedBounds, SystemUtil.getScreenBounds(screen, true));
338
339          // this screen accept current position --> no need to adjust position
340          if (pt == null)
341              return false;
342          
343          // we already have an adjusted position ?
344          if (newPos != null)
345              useMainScreen = true;
346          else
347              newPos = pt;
348      }
349
350      // multiple possible position adjustment ? --> use main screen
351      if (useMainScreen)
352          newPos = fixPosition(wantedBounds, SystemUtil.getScreenBounds(getScreen(component), true));
353
354      // got a new position ? --> set it
355      if (newPos != null)
356      {
357          wantedBounds.setLocation(newPos);
358          return true;
359      }
360
361      return false;
362
363//        final List<GraphicsDevice> screens = getScreens(component);
364//        Point newPos = null;
365//        boolean useMainScreen = false;
366//
367//        for (GraphicsDevice screen : screens)
368//        {
369//            final Point pt = fixPosition(wantedBounds, SystemUtil.getScreenBounds(screen, true));
370//
371//            // this screen accept current position --> no need to adjust position
372//            if (pt == null)
373//                return false;
374//
375//            // we already have an adjusted position ?
376//            if (newPos != null)
377//                useMainScreen = true;
378//            else
379//                newPos = pt;
380//        }
381//
382//        // use main screen
383//        if (screens.isEmpty())
384//            useMainScreen = true;
385//
386//        // multiple possible position adjustment ? --> use main screen
387//        if (useMainScreen)
388//            newPos = fixPosition(wantedBounds, SystemUtil.getScreenBounds(getScreen(component), true));
389//
390//        // got a new position ? --> set it
391//        if (newPos != null)
392//            wantedBounds.setLocation(newPos);
393//
394//        return true;
395    }
396
397    /**
398     * Fix the given bounds of specified component so it does not go completely off screen.<br>
399     * Returns <code>true</code> if component position has be adjusted.
400     */
401    public static boolean fixPosition(Component component)
402    {
403        final Rectangle bounds = component.getBounds();
404
405        if (fixPosition(component, bounds))
406        {
407            component.setBounds(bounds);
408            return true;
409        }
410
411        return false;
412    }
413
414    public static int getComponentIndex(Component c)
415    {
416        if (c != null)
417        {
418            final Container container = c.getParent();
419
420            if (container != null)
421                for (int i = 0; i < container.getComponentCount(); i++)
422                    if (container.getComponent(i) == c)
423                        return i;
424        }
425
426        return -1;
427    }
428
429    public static Point convertPoint(Component src, Point p, Component dst)
430    {
431        return SwingUtilities.convertPoint(src, p, dst);
432    }
433
434    public static Point convertPointFromScreen(Point p, Component c)
435    {
436        final Point result = new Point(p);
437
438        SwingUtilities.convertPointFromScreen(result, c);
439
440        return result;
441    }
442
443    public static Point convertPointToScreen(Point p, Component c)
444    {
445        final Point result = new Point(p);
446
447        SwingUtilities.convertPointToScreen(result, c);
448
449        return result;
450    }
451
452    public static boolean isOutside(Component c, Rectangle r)
453    {
454        return !r.intersects(c.getBounds());
455    }
456
457    public static boolean isInside(Component c, Rectangle r)
458    {
459        return r.contains(c.getBounds());
460    }
461
462    public static void increaseFontSize(Component c, int value)
463    {
464        setFontSize(c, c.getFont().getSize() + value);
465    }
466
467    public static void decreaseFontSize(Component c, int value)
468    {
469        setFontSize(c, c.getFont().getSize() - value);
470    }
471
472    public static void setFontSize(Component c, int fontSize)
473    {
474        c.setFont(FontUtil.setSize(c.getFont(), fontSize));
475    }
476
477    public static void setFontStyle(Component c, int fontStyle)
478    {
479        c.setFont(FontUtil.setStyle(c.getFont(), fontStyle));
480    }
481
482    public static void setFontBold(Component c)
483    {
484        setFontStyle(c, c.getFont().getStyle() | Font.BOLD);
485    }
486
487    public static void setJTextPaneFont(JTextPane tp, Font font, Color c)
488    {
489        final MutableAttributeSet attrs = tp.getInputAttributes();
490
491        // Set the font family, size, and style, based on properties of
492        // the Font object. Note that JTextPane supports a number of
493        // character attributes beyond those supported by the Font class.
494        // For example, underline, strike-through, super- and sub-script.
495        StyleConstants.setFontFamily(attrs, font.getFamily());
496        StyleConstants.setFontSize(attrs, font.getSize());
497        StyleConstants.setItalic(attrs, (font.getStyle() & Font.ITALIC) != 0);
498        StyleConstants.setBold(attrs, (font.getStyle() & Font.BOLD) != 0);
499
500        // Set the font color
501        StyleConstants.setForeground(attrs, c);
502
503        // Retrieve the pane's document object
504        StyledDocument doc = tp.getStyledDocument();
505
506        // Replace the style for the entire document. We exceed the length
507        // of the document by 1 so that text entered at the end of the
508        // document uses the attributes.
509        doc.setCharacterAttributes(0, doc.getLength() + 1, attrs, false);
510    }
511
512    public static void setTickMarkers(JSlider slider)
513    {
514        final int min = slider.getMinimum();
515        final int max = slider.getMaximum();
516        final int delta = max - min;
517
518        if (delta > 0)
519        {
520            final int sliderSize;
521            if (slider.getOrientation() == SwingConstants.HORIZONTAL)
522                sliderSize = slider.getPreferredSize().width;
523            else
524                sliderSize = slider.getPreferredSize().height;
525
526            // adjust ticks space on slider
527            final int majTick = findBestMajTickSpace(sliderSize, delta);
528
529            slider.setMinorTickSpacing(Math.max(1, majTick / 5));
530            slider.setMajorTickSpacing(majTick);
531            slider.setLabelTable(slider.createStandardLabels(slider.getMajorTickSpacing(), majTick));
532        }
533    }
534
535    private static int findBestMajTickSpace(int sliderSize, int delta)
536    {
537        final int values[] = {1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000};
538        // wanted a major tick each ~40 pixels
539        final int wantedMajTickSpace = delta / (sliderSize / 40);
540
541        int min = Integer.MAX_VALUE;
542        int bestValue = 1;
543
544        // try with our predefined values
545        for (int value : values)
546        {
547            final int dx = Math.abs(value - wantedMajTickSpace);
548
549            if (dx < min)
550            {
551                min = dx;
552                bestValue = value;
553            }
554        }
555
556        return bestValue;
557    }
558
559    /**
560     * Breaks the list of items in the specified menu, by creating sub-menus containing the
561     * specified number of items, and a "More..." menu to access subsequent items.
562     * 
563     * @param menu
564     *        the menu to break into smaller sub-menus
565     * @param maxItemsPerMenu
566     *        the maximum number of items to display in each sub-menu
567     */
568    public static void split(JMenu menu, int maxItemsPerMenu)
569    {
570        ArrayList<Component> components = new ArrayList<Component>(Arrays.asList(menu.getPopupMenu().getComponents()));
571
572        if (components.size() > maxItemsPerMenu)
573        {
574            menu.removeAll();
575
576            JMenu currentMenu = menu;
577
578            while (components.size() > 0)
579            {
580                int n = Math.min(components.size(), maxItemsPerMenu - 1);
581
582                for (int i = 0; i < n; i++)
583                    currentMenu.add(components.remove(0));
584
585                if (components.size() > 0)
586                    currentMenu = (JMenu) currentMenu.add(new JMenu("More..."));
587            }
588
589            if (components.size() > 0)
590                System.err.println(components.size() + " are remaining !!");
591        }
592
593        // do this recursively for sub-menus
594        for (Component component : menu.getPopupMenu().getComponents())
595        {
596            if (component instanceof JMenu)
597                split((JMenu) component, maxItemsPerMenu);
598        }
599    }
600
601    public static TreePath buildTreePath(TreeNode node)
602    {
603        final ArrayList<TreeNode> nodes = new ArrayList<TreeNode>();
604
605        nodes.add(node);
606
607        TreeNode n = node;
608        while (n.getParent() != null)
609        {
610            n = n.getParent();
611            nodes.add(n);
612        }
613
614        Collections.reverse(nodes);
615
616        return new TreePath(nodes.toArray());
617    }
618
619    public static void expandAllTree(JTree tree)
620    {
621        for (int i = 0; i < tree.getRowCount(); i++)
622            tree.expandRow(i);
623    }
624
625    public static HyperlinkListener getDefaultHyperlinkListener()
626    {
627        return new HyperlinkListener()
628        {
629            @Override
630            public void hyperlinkUpdate(HyperlinkEvent e)
631            {
632                if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
633                    NetworkUtil.openURL(e.getURL());
634            }
635        };
636    }
637
638    public static boolean isMaximized(Frame f)
639    {
640        return (f.getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH;
641    }
642
643    public static void setMaximized(Frame f, boolean b)
644    {
645        // only relevant if state changed
646        if (isMaximized(f) ^ b)
647        {
648            if (b)
649                f.setExtendedState(Frame.MAXIMIZED_BOTH);
650            else
651                f.setExtendedState(Frame.NORMAL);
652        }
653    }
654
655    public static boolean isMinimized(Frame f)
656    {
657        return (f.getExtendedState() & Frame.ICONIFIED) == Frame.ICONIFIED;
658    }
659
660    public static void setMinimized(Frame f, boolean b)
661    {
662        // only relevant if state changed
663        if (isMinimized(f) ^ b)
664        {
665            if (b)
666                f.setExtendedState(Frame.ICONIFIED);
667            else
668                f.setExtendedState(Frame.NORMAL);
669        }
670    }
671}