/* This file is part of leafdigital leafChat. leafChat is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. leafChat is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with leafChat. If not, see <http://www.gnu.org/licenses/>. Copyright 2012 Samuel Marshall. */ package com.leafdigital.ui; import java.awt.*; import java.awt.event.*; import java.awt.image.BufferedImage; import javax.swing.*; import util.GraphicsUtils; import com.leafdigital.ui.api.UI; import leafchat.core.api.BugException; /** * The bar at the bottom of the main window which lets you switch between * windows. */ public class SwitchBar extends JPanel { /** Size index of buttons */ private int buttonSize=1; /** Normal and attention colours */ static Color normalRGB=Color.black,attentionRGB=Color.red,minRGB=Color.gray; /** Button fonts */ private Font tabButtonFont,tabButtonFontBold; /** UI (for accessing theme) */ private UISingleton ui; SwitchBar(UISingleton ui) { this.ui=ui; setLayout(null); setOpaque(true); // Get default button font size (0.7 * JButton font) JButton fontSample = new JButton("Hello"); int defaultSize=Math.round(0.7f * fontSample.getFont().getSize()); // Find most suited button set for(buttonSize=1;buttonSize<=ui.getTheme().getIntProperty("tabs","numSizes",1);buttonSize++) { int fontSize=getIntProperty("titleSize"); if(fontSize==0) // Run out of sizes { buttonSize--; break; } else if(fontSize >= defaultSize) { // First one bigger than our desired size will do break; } } // Set up fonts tabButtonFont=fontSample.getFont().deriveFont((float)getIntProperty("titleSize")); tabButtonFontBold=tabButtonFont.deriveFont(Font.BOLD); // Listen for keypresses. // I tried to do this with a normal input map but couldn't get it to work! // It basically worked but not if you minimised all the windows - that left // focus in some bizarre state. // Note that this evil code means it's important SwitchBar is not // constructed more than once, so we check if(constructedAlready) throw new BugException( "SwitchBar may not be constructed more than once"); constructedAlready=true; KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher( new KeyEventDispatcher() { @Override public boolean dispatchKeyEvent(KeyEvent e) { if( (e.getID()==KeyEvent.KEY_PRESSED) && ((e.getModifiers() & (KeyEvent.ALT_MASK | KeyEvent.SHIFT_MASK | KeyEvent.META_MASK | KeyEvent.CTRL_MASK))==KeyEvent.ALT_MASK) && (e.getKeyCode() >= KeyEvent.VK_0) && (e.getKeyCode() <= KeyEvent.VK_9) && isVisible()) { int key=e.getKeyCode()-KeyEvent.VK_0; clickButton(key==0 ? 9 : key-1); return true; } else return false; } }); } private static boolean constructedAlready; /** * Clicks on a specific button. * @param index Index (0-based) of button. May not exist in which case * nothing happens */ private void clickButton(int index) { Component[] ac=getComponents(); if(ac.length<=index) return; ((SwitchButton)ac[index]).click(); } int uiStyle=UI.UISTYLE_SINGLEWINDOW; void setUIStyle(int style) { this.uiStyle=style; } @Override public void paint(Graphics g) { if(uiStyle==UI.UISTYLE_TABBED) { // Paint background BufferedImage bg=getImageProperty("background"); for(int x=0;x<getWidth();x+=bg.getWidth()) { g.drawImage(bg,x,0,null); } // Paint all the background switchbuttons Component[] ac=getComponents(); for(int i=0;i<ac.length;i++) { if((ac[i] instanceof TabSwitchButton) && !((TabSwitchButton)ac[i]).isFG()) { paintChild(g,ac[i]); } } // Paint midground BufferedImage mid=getImageProperty("midground"); for(int x=0;x<getWidth();x+=mid.getWidth()) { g.drawImage(mid,x,0,null); } // Paint all the other components for(int i=0;i<ac.length;i++) { if(!((ac[i] instanceof TabSwitchButton) && !((TabSwitchButton)ac[i]).isFG())) { paintChild(g,ac[i]); } } } else { // Paint background BufferedImage bg=getImageProperty("buttonBackground"); for(int x=0;x<getWidth();x+=bg.getWidth()) { g.drawImage(bg,x,0,null); } paintChildren(g); } } private void paintChild(Graphics g,Component c) { g.setColor(c.getForeground()); g.setFont(c.getFont()); Rectangle bounds=c.getBounds(); Shape clipBefore=g.getClip(); g.clipRect(bounds.x,bounds.y,bounds.width,bounds.height); g.translate(bounds.x,bounds.y); c.paint(g); g.translate(-bounds.x,-bounds.y); g.setClip(clipBefore); } @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); rearrangeButtons(); } void rearrangeButtons() { Insets i=getInsets(); Component[] ac=getComponents(); if(ac.length==0) return; int gap=uiStyle==UI.UISTYLE_SINGLEWINDOW ? getIntProperty("buttonGap") : getIntProperty("gap"); int iButtonWidth=(getWidth()-i.left-i.right-2*getIntProperty("gap"))/ac.length; int preferredWidth=uiStyle==UI.UISTYLE_SINGLEWINDOW ? getIntProperty("buttonWidth") : getIntProperty("width"); if(iButtonWidth>preferredWidth) iButtonWidth=preferredWidth; if(iButtonWidth<preferredWidth/4) iButtonWidth=preferredWidth/4; int iX=i.left+gap; for (int iComponent= 0; iComponent < ac.length; iComponent++) { ac[iComponent].setBounds(iX,i.top, iButtonWidth-gap,getHeight()-i.top-i.bottom); iX+=iButtonWidth; } } private void informMoved() { Component[] ac=getComponents(); for(int i=0;i<ac.length;i++) { if(ac[i] instanceof SwitchButton) { SwitchButton sb=(SwitchButton)ac[i]; if(sb.hasAttention()) { ((JComponent)sb).repaint(); } } } } @Override public Dimension getPreferredSize() { return new Dimension(100,getIntProperty("height")); } void addFrame(FrameInside fi) { add(new InsideSwitchButton(fi)); rearrangeButtons(); } void removeFrame(FrameHolder fh) { Component[] ac=getComponents(); for (int iComponent= 0; iComponent < ac.length; iComponent++) { SwitchButton sb=(SwitchButton)ac[iComponent]; if(sb.getFrame()==fh) sb.close(); } rearrangeButtons(); } /** * Adds a tab to the switchbar. * @param tab Tab */ public void addFrame(FrameTab tab) { add(new TabSwitchButton(tab)); rearrangeButtons(); } private FrameHolder active=null; void informActiveFrame(FrameHolder active) { if(this.active==active) return; this.active=active; repaint(); } FrameHolder getActiveFrame() { return active; } private int getIntProperty(String name) { return ui.getTheme().getIntProperty("tabs",name+buttonSize,0); } private BufferedImage getImageProperty(String name) { return ui.getTheme().getImageProperty("tabs",name+buttonSize,true,null,null); } class InsideSwitchButton extends JComponent implements SwitchButton { private FrameInside fi; private JLabel title; private boolean attention; private long attentionTime; InsideSwitchButton(FrameInside fiSet) { this.fi=fiSet; fi.setSwitchButton(this); setPreferredSize(new Dimension(200,getIntProperty("height"))); setLayout(null); title=new JLabel(fi.getTitle()); title.setFont(tabButtonFont); add(title); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { click(); } }); addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { if(e.getKeyChar()==' ' || e.getKeyChar()=='\n') { click(); } } }); setFocusable(true); } @Override public void click() { if(!fi.isVisible()) { fi.handleRestore(); fi.focusFrame(); } else if(isActive()) { fi.handleMinimize(); } else { fi.focusFrame(); } } @Override public void setBounds(int x,int y,int width,int height) { super.setBounds(x,y,width,height); int ascent=(int)tabButtonFont.getLineMetrics("Aj", GraphicsUtils.getFontRenderContext()).getAscent(); title.setBounds( getIntProperty("buttonTitleLeftOffset"), height-getIntProperty("buttonTitleBaselineOffset")-ascent, getWidth()-getIntProperty("buttonTitleLeftOffset")-getIntProperty("buttonTitleRightOffset"), title.getPreferredSize().height ); } private void checkActiveState(boolean newActive) { if(newActive!=paintedActive) { paintedActive=newActive; title.setFont(paintedActive ? tabButtonFontBold : tabButtonFont); } if(attention && fi.canClearAttention()) { if(paintedActive || fi.isReasonablyVisible()) attention=false; } Color rgb=attention ? attentionRGB : !fi.isVisible() ? minRGB : normalRGB; if(title.getForeground()!=rgb) title.setForeground(rgb); } boolean paintedActive=false; boolean isActive() { return fi==active && fi.isVisible(); } @Override protected void paintComponent(Graphics g) { // Set up button if needed boolean active=isActive(); checkActiveState(active); String activeText=active ? "Active" : "Inactive"; // Draw background BufferedImage left=getImageProperty("buttonLeft"+activeText), middle=getImageProperty("buttonMiddle"+activeText), right=getImageProperty("buttonRight"+activeText); g.drawImage(left,0,0,null); int rightPos=getWidth()-right.getWidth(); Shape s=g.getClip(); g.clipRect(0,0,rightPos,getHeight()); for(int pos=left.getWidth();pos<500;pos+=middle.getWidth()) { g.drawImage(middle,pos,0,null); } g.setClip(s); g.drawImage(right,rightPos,0,null); if(hasFocus()) GraphicsUtils.drawFocus((Graphics2D)g,0,0,getWidth(),getHeight()); } @Override public void attention() { attentionTime = System.currentTimeMillis(); if(attention) { return; } attention = true; repaint(); } @Override public long getAttentionTime() { return attention ? attentionTime : 0; } @Override public boolean hasAttention() { return attention; } @Override public void close() { getParent().remove(this); SwitchBar.this.repaint(); } @Override public FrameHolder getFrame() { return fi; } @Override public void informChanged() { String newTitle=fi.getTitle(); if(!title.getText().equals(newTitle)) title.setText(newTitle); repaint(); } @Override public void informMoved() { SwitchBar.this.informMoved(); } } class TabSwitchButton extends JComponent implements SwitchButton { private FrameTab ft; private JLabel title; private JButton close; private boolean attention; private long attentionTime; TabSwitchButton(FrameTab ft) { this.ft=ft; ft.setSwitchButton(this); setPreferredSize(new Dimension(200,getIntProperty("height"))); setLayout(null); title=new JLabel(ft.getTitle()); title.setFont(tabButtonFont); add(title); close=new JButton(); close.setOpaque(false); close.setRolloverEnabled(true); close.setBorderPainted(false); close.setContentAreaFilled(false); close.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { TabSwitchButton.this.ft.handleClose(); } }); add(close); checkFGState(true); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { click(); } }); addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { if(e.getKeyChar()==' ' || e.getKeyChar()=='\n') { click(); } } }); setFocusable(true); } @Override public void click() { TabSwitchButton.this.ft.focusFrame(); } @Override public void setBounds(int x,int y,int width,int height) { super.setBounds(x,y,width,height); int closeLeft; if(close.isVisible()) { BufferedImage closeImg=getImageProperty("closeFG"); closeLeft=getWidth()-closeImg.getWidth()-getIntProperty("closeRightOffset"); close.setBounds( closeLeft, height-closeImg.getHeight()-getIntProperty("closeBottomOffset"), closeImg.getWidth(),closeImg.getHeight()); } else { closeLeft=getWidth(); } Font f=title.getFont(); int ascent=(int)f.getLineMetrics("Aj",GraphicsUtils.getFontRenderContext()).getAscent(); title.setBounds( getIntProperty("titleLeftOffset"), height-getIntProperty("titleBaselineOffset")-ascent, closeLeft-getIntProperty("titleLeftOffset")-getIntProperty("titleRightOffset"), title.getPreferredSize().height ); } private void checkFGState(boolean newFG) { if(newFG!=paintedFG) { paintedFG=newFG; String fg=newFG ? "FG" : "BG"; close.setIcon(new ImageIcon(getImageProperty("close"+fg))); close.setRolloverIcon(new ImageIcon(getImageProperty("closeHover"+fg))); title.setFont(paintedFG ? tabButtonFontBold : tabButtonFont); repaint(); } if(attention && paintedFG && ft.canClearAttention()) { title.setForeground(normalRGB); attention=false; repaint(); } } boolean paintedFG=false; boolean isFG() { return ft==active; } @Override protected void paintComponent(Graphics g) { // Set up button if needed boolean fg=isFG(); checkFGState(fg); String fgText=fg ? "FG" : "BG"; // Draw background BufferedImage left=getImageProperty("left"+fgText), middle=getImageProperty("middle"+fgText), right=getImageProperty("right"+fgText); g.drawImage(left,0,0,null); int rightPos=getWidth()-right.getWidth(); Shape s=g.getClip(); g.clipRect(0,0,rightPos,getHeight()); for(int pos=left.getWidth();pos<500;pos+=middle.getWidth()) { g.drawImage(middle,pos,0,null); } g.setClip(s); g.drawImage(right,rightPos,0,null); if(hasFocus()) GraphicsUtils.drawFocus((Graphics2D)g,0,0,getWidth(),getHeight()); } @Override public void attention() { attentionTime = System.currentTimeMillis(); if(attention) return; title.setForeground(attentionRGB); attention=true; repaint(); } @Override public long getAttentionTime() { return attention ? attentionTime : 0; } @Override public boolean hasAttention() { return attention; } @Override public void close() { getParent().remove(this); SwitchBar.this.repaint(); } @Override public FrameHolder getFrame() { return ft; } @Override public void informChanged() { String newTitle=ft.getTitle(); if(!title.getText().equals(newTitle)) title.setText(newTitle); if(ft.isClosable()!=close.isVisible()) { close.setVisible(ft.isClosable()); Rectangle r=getBounds(); setBounds(r.x,r.y,r.width,r.height); } } @Override public void informMoved() { SwitchBar.this.informMoved(); } } interface SwitchButton { FrameHolder getFrame(); void informChanged(); void informMoved(); void attention(); /** * If the button has attention, returns the last time it was attentioned; * otherwise returns 0. * @return Time at which button was most recently attentioned */ long getAttentionTime(); void close(); boolean hasAttention(); void click(); } }