/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.plugin.desktoputil;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.net.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import net.java.sip.communicator.service.keybindings.*;
import net.java.sip.communicator.util.Logger;
import org.jitsi.service.configuration.*;
import org.jitsi.service.resources.*;
import org.jitsi.util.*;
/**
* A custom frame that remembers its size and location and could have a
* semi-transparent background.
*
* @author Yana Stamcheva
* @author Lyubomir Marinov
* @author Adam Netocny
*/
public class SIPCommFrame
extends JFrame
implements Observer
{
/**
* Serial version UID.
*/
private static final long serialVersionUID = 0L;
/**
* Property that disables the automatic resizing and positioning when a
* window's top edge is outside the visible area of the screen.
* <p>
* <tt>true</tt> use automatic repositioning (default)<br/>
* <tt>false</tt> rely on the window manager to place the window
*/
static final String PNAME_CALCULATED_POSITIONING
= "net.sip.communicator.util.swing.USE_CALCULATED_POSITIONING";
/**
* The <tt>Logger</tt> used by the <tt>SIPCommFrame</tt> class and its
* instances for logging output.
*/
private static final Logger logger = Logger.getLogger(SIPCommFrame.class);
/**
* The action map of this dialog.
*/
private ActionMap amap;
/**
* The input map of this dialog.
*/
private InputMap imap;
/**
* The key bindings set.
*/
private KeybindingSet bindings = null;
/**
* Indicates if the size of this dialog is stored after
* closing. By default we store window size and location.
*/
private boolean saveSize = true;
/**
* Indicates if the location of this dialog is stored after
* closing. By default we store window size and location.
*/
private boolean saveLocation = true;
/**
* Creates a <tt>SIPCommFrame</tt>.
*/
public SIPCommFrame()
{
// If on MacOS we would use the native background.
if (!OSUtils.IS_MAC)
setContentPane(new MainContentPane());
init();
addWindowListener(new FrameWindowAdapter());
JRootPane rootPane = getRootPane();
amap = rootPane.getActionMap();
amap.put("close", new CloseAction());
amap.put("closeEsc", new CloseEscAction());
imap = rootPane.getInputMap(
JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "closeEsc");
// put the defaults for macosx
if(OSUtils.IS_MAC)
{
imap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.META_DOWN_MASK),
"closeEsc");
imap.put(
KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK),
"closeEsc");
}
WindowUtils.addWindow(this);
}
/**
* Initialize default values.
*/
private void init()
{
updateIconImages(this);
}
/**
* Sets the list of icons to the <tt>window</tt>.
* @param window the window, which icons will be updated.
*/
public static void updateIconImages(Window window)
{
try
{
List<Image> logos = new ArrayList<Image>(6)
{
private static final long serialVersionUID = 0L;
{
add(DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO"));
add(DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO_20x20"));
add(DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO_32x32"));
add(DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO_45x45"));
add(DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO_64x64"));
add(DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO_128x128"));
}
};
window.setIconImages(logos);
// In order to have the same icon when using option panes
JOptionPane.getRootFrame().setIconImages(logos);
}
catch (Exception e)
{
Image scLogo = DesktopUtilActivator.getImage(
"service.gui.SIP_COMMUNICATOR_LOGO");
window.setIconImage(scLogo);
// In order to have the same icon when using option panes
JOptionPane.getRootFrame().setIconImage(scLogo);
}
}
/**
* Creates an instance of <tt>SIPCommFrame</tt> by specifying explicitly
* if the size and location properties are saved. By default size and
* location are stored.
* @param saveSizeAndLocation indicates whether to save the size and
* location of this dialog
*/
public SIPCommFrame(boolean saveSizeAndLocation)
{
this(saveSizeAndLocation, saveSizeAndLocation);
}
/**
* Creates an instance of <tt>SIPCommFrame</tt> by specifying explicitly
* if the size and location properties are saved. By default size and
* location are stored.
* @param saveLocation indicates whether to save the
* location of this dialog
* @param saveSize indicates whether to save the size of this dialog
*/
public SIPCommFrame(boolean saveLocation, boolean saveSize)
{
this();
this.saveLocation = saveLocation;
this.saveSize = saveSize;
}
/**
* The action invoked when user presses Ctrl-W and Cmd-W key combination.
*/
private class CloseAction
extends UIAction
{
/**
* Serial version UID.
*/
private static final long serialVersionUID = 0L;
public void actionPerformed(ActionEvent e)
{
if (saveLocation || saveSize)
saveSizeAndLocation();
close(false);
}
}
/**
* The action invoked when user presses Escape key.
*/
private class CloseEscAction
extends UIAction
{
/**
* Serial version UID.
*/
private static final long serialVersionUID = 0L;
public void actionPerformed(ActionEvent e)
{
if (saveLocation || saveSize)
saveSizeAndLocation();
close(true);
}
}
/**
* Sets the input map to utilize a given category of keybindings. The frame
* is updated to reflect the new bindings when they change. This replaces
* any previous bindings that have been added.
*
* @param category set of keybindings to be utilized
*/
protected void setKeybindingInput(KeybindingSet.Category category)
{
// Removes old binding set
if (bindings != null)
{
bindings.deleteObserver(this);
resetInputMap();
}
// Adds new bindings to input map
bindings
= DesktopUtilActivator.getKeybindingsService().getBindings(category);
if (bindings != null)
{
for (Map.Entry<KeyStroke, String> key2action
: bindings.getBindings().entrySet())
imap.put(key2action.getKey(), key2action.getValue());
bindings.addObserver(this);
}
}
/**
* Bindings the string representation for a keybinding to the action that
* will be executed.
*
* @param binding string representation of action used by input map
* @param action the action which will be executed when user presses the
* given key combination
*/
protected void addKeybindingAction(String binding, Action action)
{
amap.put(binding, action);
}
private static class FrameWindowAdapter
extends WindowAdapter
{
@Override
public void windowClosing(WindowEvent e)
{
((SIPCommFrame) e.getWindow()).windowClosing(e);
}
}
/**
* Invoked when this window is in the process of being closed. The close
* operation can be overridden at this point.
* @param e the <tt>WindowEvent</tt> that notified us
*/
protected void windowClosing(WindowEvent e)
{
/*
* Before closing the application window save the current size and
* position through the ConfigurationService.
*/
if(saveLocation || saveSize)
saveSizeAndLocation();
close(false);
}
/**
* Invokes the {@link Window#dispose()} implementation of this instance
* thus skipping any overriding that may be in effect for the method in
* question by extenders.
*/
protected void windowDispose()
{
super.dispose();
}
/**
* Saves the size and the location of this frame through the
* <tt>ConfigurationService</tt>.
*/
private void saveSizeAndLocation()
{
try
{
saveSizeAndLocation(this, saveSize, saveLocation);
}
catch (ConfigPropertyVetoException e)
{
logger
.error(
"Saving the size and the location properties failed",
e);
}
}
/**
* Saves the size and the location of a specific <tt>Component</tt> through
* the <tt>ConfigurationService</tt>.
*
* @param component the <tt>Component</tt> which is to have its size and
* location saved through the <tt>ConfigurationService</tt>
* @throws ConfigPropertyVetoException if the <tt>ConfigurationService</tt>
* does not accept the saving because of objections from its
* <tt>PropertyVetoListener</tt>s.
*/
static void saveSizeAndLocation(Component component,
boolean saveSize,
boolean saveLocation)
throws ConfigPropertyVetoException
{
Map<String, Object> props = new HashMap<String, Object>();
String className
= component.getClass().getName().replaceAll("\\$", "_");
if(saveSize)
{
props.put(className + ".width", component.getWidth());
props.put(className + ".height", component.getHeight());
}
if(saveLocation)
{
props.put(className + ".x", component.getX());
props.put(className + ".y", component.getY());
}
DesktopUtilActivator.getConfigurationService().setProperties(props);
}
/**
* Sets window size and position.
*/
public void setSizeAndLocation()
{
if (!(saveLocation || saveSize))
{
return;
}
ConfigurationService configService =
DesktopUtilActivator.getConfigurationService();
String className = this.getClass().getName();
if(saveSize)
{
String widthString
= configService.getString(className + ".width");
String heightString
= configService.getString(className + ".height");
if(widthString != null && heightString != null)
{
int width = Integer.parseInt(widthString);
int height = Integer.parseInt(heightString);
if(width > 0 && height > 0)
{
Dimension screenSize =
Toolkit.getDefaultToolkit().getScreenSize();
if(width <= screenSize.width && height <= screenSize.height)
this.setSize(width, height);
}
}
}
if(saveLocation)
{
String xString = configService.getString(className + ".x");
String yString = configService.getString(className + ".y");
if(xString != null && yString != null)
{
int x = Integer.parseInt(xString);
int y = Integer.parseInt(yString);
if(ScreenInformation.
isTitleOnScreen(
new Rectangle(x, y, this.getWidth(), this.getHeight()))
|| configService.getBoolean(
SIPCommFrame.PNAME_CALCULATED_POSITIONING, true))
{
this.setLocation(x, y);
}
}
else
{
this.setCenterLocation();
}
}
}
/**
* Positions this window in the center of the screen.
*/
private void setCenterLocation()
{
setLocationRelativeTo(null);
}
/**
* Checks whether the current component will exceeds the screen size and if
* it do will set a default size
*/
private void ensureOnScreenLocationAndSize()
{
ConfigurationService config = DesktopUtilActivator.getConfigurationService();
if(!config.getBoolean(SIPCommFrame.PNAME_CALCULATED_POSITIONING, true))
return;
int x = this.getX();
int y = this.getY();
int width = this.getWidth();
int height = this.getHeight();
Rectangle virtualBounds = ScreenInformation.getScreenBounds();
// the default distance to the screen border
final int borderDistance = 10;
// in case any of the sizes exceeds the screen size
// we set default one
// get the left upper point of the window
if (!(virtualBounds.contains(x, y)))
{
// top left exceeds screen bounds
if (x < virtualBounds.x)
{
// window is too far to the left
// move it to the right
x = virtualBounds.x + borderDistance;
}
else if (x > virtualBounds.x)
{
// window is too far to the right
// can only occour, when screen resolution is
// changed or displayed are disconnected
// move the window in the bounds to the very right
x =
virtualBounds.x + virtualBounds.width - width
- borderDistance;
if (x < virtualBounds.x + borderDistance)
{
x = virtualBounds.x + borderDistance;
}
}
// top left exceeds screen bounds
if (y < virtualBounds.y)
{
// window is too far to the top
// move it to the bottom
y = virtualBounds.y + borderDistance;
}
else if (y > virtualBounds.y)
{
// window is too far to the bottom
// can only occour, when screen resolution is
// changed or displayed are disconnected
// move the window in the bounds to the very bottom
y =
virtualBounds.y + virtualBounds.height - height
- borderDistance;
if (y < virtualBounds.y + borderDistance)
{
y = virtualBounds.y + borderDistance;
}
}
this.setLocation(x, y);
}
// check the lower right corder
if (!(virtualBounds.contains(x + width, y + height)))
{
if (x + width > virtualBounds.x + virtualBounds.width)
{
// location of window is too far to the right, its right
// border is out of bounds
// calculate a new horizontal position
// move the whole window to the left
x =
virtualBounds.x + virtualBounds.width - width
- borderDistance;
if (x < virtualBounds.x + borderDistance)
{
// window is already on left side, it is too wide.
x = virtualBounds.x + borderDistance;
// reduce the width, so it surely fits
width = virtualBounds.width - 2 * borderDistance;
}
}
if (y + height > virtualBounds.y + virtualBounds.height)
{
// location of window is too far to the bottom, its bottom
// border is out of bounds
// calculate a new vertical position
// move the whole window to the top
y =
virtualBounds.y + virtualBounds.height - height
- borderDistance;
if (y < virtualBounds.y + borderDistance)
{
// window is already on top, it is too high.
y = virtualBounds.y + borderDistance;
// reduce the width, so it surely fits
height = virtualBounds.height - 2 * borderDistance;
}
}
this.setPreferredSize(new Dimension(width, height));
this.setSize(width, height);
this.setLocation(x, y);
}
}
/**
* Overwrites the setVisible method in order to set the size and the
* position of this window before showing it.
* @param isVisible indicates if this frame should be visible
*/
@Override
public void setVisible(boolean isVisible)
{
if (isVisible)
{
this.setSizeAndLocation();
this.ensureOnScreenLocationAndSize();
}
super.setVisible(isVisible);
}
/**
* Overwrites the setVisible method in order to set the size and the
* position of this window before showing it.
* @param isVisible indicates if this window will be made visible or will
* be hidden
* @param isPackEnabled indicates if the pack() method should be invoked
* before showing this window
*/
public void setVisible(boolean isVisible, boolean isPackEnabled)
{
if (isVisible)
{
/*
* Since setSizeAndLocation() will use the width and the height,
* pack() should be called prior to it. Otherwise, the width and the
* height may be zero or may just change after setSizeAndLocation()
* during pack().
*/
this.pack();
this.setSizeAndLocation();
this.ensureOnScreenLocationAndSize();
}
super.setVisible(isVisible);
}
/**
* {@inheritDoc}
*
* Overwrites the super's <tt>dispose</tt> method in order to save the size
* and the position of this <tt>Window</tt> before closing it.
*/
@Override
public void dispose()
{
if (saveLocation || saveSize)
saveSizeAndLocation();
/*
* The KeybindingsService will outlive us so don't let us retain our
* memory.
*/
if (bindings != null)
bindings.deleteObserver(this);
super.dispose();
}
private void resetInputMap()
{
imap.clear();
imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "close");
}
/**
* Listens for changes in binding sets so they can be reflected in the input
* map.
* @param obs the <tt>KeybindingSet</tt> from which to update
*/
public void update(Observable obs, Object arg)
{
if (obs instanceof KeybindingSet)
{
KeybindingSet changedBindings = (KeybindingSet) obs;
resetInputMap();
for (Map.Entry<KeyStroke, String> key2action : changedBindings
.getBindings().entrySet())
{
imap.put(key2action.getKey(), key2action.getValue());
}
}
}
/**
* The main content pane.
*/
public static class MainContentPane
extends JPanel
{
/**
* Serial version UID.
*/
private static final long serialVersionUID = 0L;
private boolean isColorBgEnabled;
private boolean isImageBgEnabled;
private Color bgStartColor;
private Color bgEndColor;
private BufferedImage bgImage = null;
private TexturePaint texture = null;
/**
* Creates an instance of <tt>MainContentPane</tt>.
*/
public MainContentPane()
{
super(new BorderLayout());
initColors();
initStyles();
}
/**
* Validates this container and all of its subcomponents.
* <p>
* The <code>validate</code> method is used to cause a container
* to lay out its subcomponents again. It should be invoked when
* this container's subcomponents are modified (added to or
* removed from the container, or layout-related information
* changed) after the container has been displayed.
*
* <p>If this {@code Container} is not valid, this method invokes
* the {@code validateTree} method and marks this {@code Container}
* as valid. Otherwise, no action is performed.
*
* @see #add(java.awt.Component)
* @see Component#invalidate
* @see javax.swing.JComponent#revalidate()
* @see #validateTree
*/
@Override
public void validate()
{
initStyles();
super.validate();
}
/**
* Repaints this component.
*/
@Override
public void repaint()
{
initColors();
super.repaint();
}
/**
* Initialize color values.
*/
private void initColors()
{
ResourceManagementService resources =
DesktopUtilActivator.getResources();
isColorBgEnabled =
new Boolean(resources.getSettingsString(
"impl.gui.IS_WINDOW_COLOR_BACKGROUND_ENABLED"))
.booleanValue();
if (isColorBgEnabled)
{
bgStartColor =
new Color(resources.getColor("service.gui.MAIN_BACKGROUND"));
bgEndColor =
new Color(resources
.getColor("service.gui.MAIN_BACKGROUND_GRADIENT"));
}
else
{
bgStartColor = null;
bgEndColor = null;
}
isImageBgEnabled =
new Boolean(resources.getSettingsString(
"impl.gui.IS_WINDOW_IMAGE_BACKGROUND_ENABLED"))
.booleanValue();
if (isImageBgEnabled)
{
final URL bgImagePath
= resources.getImageURL("service.gui.WINDOW_TITLE_BAR_BG");
bgImage = ImageUtils.getBufferedImage(bgImagePath);
final Rectangle rect =
new Rectangle(0, 0, bgImage.getWidth(),
bgImage.getHeight());
texture = new TexturePaint(bgImage, rect);
}
}
/**
* Initialize style values.
*/
private void initStyles()
{
ResourceManagementService resources =
DesktopUtilActivator.getResources();
int borderSize =
resources
.getSettingsInt("impl.gui.MAIN_WINDOW_BORDER_SIZE");
this.setBorder(BorderFactory.createEmptyBorder(borderSize,
borderSize, borderSize, borderSize));
}
/**
* Paints this content pane.
* @param g the <tt>Graphics</tt> object used for painting
*/
@Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
// If the custom color or image window background is not enabled we
// have nothing to do here.
if (isColorBgEnabled || isImageBgEnabled)
{
g = g.create();
try
{
internalPaintComponent(g);
}
finally
{
g.dispose();
}
}
}
/**
* Provides a custom paint if the color or image background properties
* are enabled.
* @param g the <tt>Graphics</tt> object used for painting
*/
private void internalPaintComponent(Graphics g)
{
AntialiasingManager.activateAntialiasing(g);
Graphics2D g2 = (Graphics2D) g;
int width = getWidth();
int height = getHeight();
if (isColorBgEnabled)
{
GradientPaint bgGradientColor =
new GradientPaint(width / 2, 0, bgStartColor, width / 2, 80,
bgEndColor);
g2.setPaint(bgGradientColor);
g2.fillRect(0, 0, width, 80);
g2.setColor(bgEndColor);
g2.fillRect(0, 78, width, height);
}
if (isImageBgEnabled)
{
if (bgImage != null && texture != null)
{
g2.setPaint(texture);
g2.fillRect(0, 0, this.getWidth(), bgImage.getHeight());
}
}
}
}
/**
* Notifies this instance that it has been requested to close. The default
* <tt>SIPCommFrame</tt> implementation does nothing.
*
* @param escape <tt>true</tt> if the request to close this instance is in
* response of a press on the Escape key; otherwise, <tt>false</tt>
*/
protected void close(boolean escape)
{
}
}