/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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 com.motorola.studio.android.emulator.skin.android;
import static com.motorola.studio.android.common.log.StudioLogger.error;
import static com.motorola.studio.android.common.log.StudioLogger.warn;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Properties;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Shell;
import com.motorola.studio.android.adt.SdkUtils;
import com.motorola.studio.android.common.log.StudioLogger;
import com.motorola.studio.android.emulator.EmulatorPlugin;
import com.motorola.studio.android.emulator.core.exception.SkinException;
import com.motorola.studio.android.emulator.core.skin.AndroidPressKey;
import com.motorola.studio.android.emulator.core.skin.IAndroidKey;
import com.motorola.studio.android.emulator.i18n.EmulatorNLS;
import com.motorola.studio.android.emulator.skin.android.parser.LayoutFileModel;
/**
* DESCRIPTION:
* Utility class for translating Google skin files data into objects
* suitable to the viewer. This includes:
* - Generating image data objects in memory that represent the exact images that
* will be drawn using SWT Image/GC/Transform graphic objects. It is necessary to
* create new image data objects from scratch because the operations performed by
* Image/GC/Transform affects only the display, keeping the associated image data
* objects intact. It would also be very expensive to generate those images at
* screen, after every mouse event (including mouse moves, which are frequent)
* - Generating a key data collection suitable for a given layout. The key data
* objects must contain positioning information that depends on the position of
* elements inside the layout. It is this class job to discover what are the
* coordinates to set at the keys
* - Translate the display position for a given layout. As with the key data
* collection, the coordinates of the display depends on the layout description
* and must be calculated.
*
* RESPONSIBILITY:
* Provide translation functionalities to convert Google format to a format
* suitable to the viewer
*
* COLABORATORS:
* None.
*
* USAGE:
* This class is intended to be used by AndroidSkin class only
*
*
*
* This class uses several concepts, that are described below:
*
* LAYOUT FILE MODEL: A representation of the file "layout", saved as part
* of the skin in the skin folder. Attributes such as keyboard charmap and
* network are global to a layout file
* LAYOUT: A collection of parts, with some part integration features, such
* as part positioning and part rotation. Parts can be rotated independently
* in a layout.
* PART: The minimum skin view. It contains buttons and background image.
* A display is optional
* OFFSET: If a part is declared to be drawn in a layout using negative coordinates,
* or if a button is declared to be drawn in a part using negative coordinates,
* the calculated layout/part offset is different from 0 in either (x, y) directions.
* If such situation happen, it is mandatory to generate a bigger image in memory
* to have the layout/part drawn.
* EXPANDED PART IMAGE: Is how is called the image generated in memory due to part
* offset being different from zero. Layouts always have images generated in memory,
* but parts can be represented as a simple file load if no button demands an offset
* different from (0, 0)
*
* Considerations about the differences between Google format and viewer format:
*
* a) Google format supports negative coordinates for parts/buttons. To generate images
* that are renderable by SWT, the viewer module must translate the parts/buttons so
* that they fit in a single image data
* b) Viewer demands a pair of pressed, released and enter images and a set of IAndroidKeys.
* Google provides several images to use as filter when a button is pressed. To increase
* the performance, all the images/keys are generated during instance start time, being
* reused afterwards
* c) In Google format, the part position at the layout is ALWAYS set to the upper-left
* corner of the PART, not the layout. Therefore, depending on the rotation parameter
* we need to calculate what is the position to draw the part relative to the layout
*
* ---------------------------
* | | 1) OLX / OLY : Offset due to layout, if one or more part
* | ------------------ | position coordinates are less than 0
* | | ------------ | | 2) OPX / OPY : Offset due to part, if one of more button
* | | | | | | position coordinates are less than 0
* | |OPX| | | | 3) LAYOUT : Layout image area
* | | ----- | | | 4) PART : Part expanded image area
* | | |BTN| | | | 5) BACKGR : Original part background image, which demanded
* | | | | | | | expansion due to BTN
* | | ----- | | | 6) BTN : A button with negative x coordinate related to
* | | | BACKGR | | | BACKGR
* | | ------------ | |
* | | PART | |
* | OLX ------------------ |
* | LAYOUT |
* ---------------------------
*
*/
public class AndroidSkinTranslator
{
/**
* Constant that describes a QWERTY charmap in the layout
*/
private static final String CHARMAP_QWERTY = "qwerty";
/**
* Constant that describes a QWERTY2 charmap in the layout
*/
private static final String CHARMAP_QWERTY2 = "qwerty2";
/**
* Path to the QWERTY codes inside the plugin
*/
private static final String CHARMAP_QWERTY_FILE = "res/qwerty.properties";
/**
* Constant that describes a AVRCP charmap in the layout
*/
private static final String CHARMAP_AVRCP = "AVRCP";
/**
* Path to the AVRCP codes inside the plugin
*/
private static final String CHARMAP_AVRCP_FILE = "res/AVRCP.properties";
private static final String CHARMAP_OPHONE_QWERTY_FILE = "res/ophone_qwerty.properties";
private static final String CHARMAP_OPHONE_AVRCP_FILE = "res/ophone_AVRCP.properties";
/**
* d-pad keys
*/
private static final String DPAD_DOWN = "DPAD_DOWN";
private static final String DPAD_UP = "DPAD_UP";
private static final String DPAD_LEFT = "DPAD_LEFT";
private static final String DPAD_RIGHT = "DPAD_RIGHT";
/**
* Translates the button information inside the layout file model into
* viewer compatible IAndroidKeys
* <br><br>
* @param layoutFile The model that represent the layout file
* @param layoutName The name of the layout where to look for buttons in the model,
* or <code>null</code> if no layout is available
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return A collection of IAndroidKeys, describing the keys, codes and positions
* of the buttons in the skin
* <br><br>
* @throws SkinException If an error that prevents the translation happens, such as
* errors when opening required files or charmap not supported
*/
static Collection<IAndroidKey> generateAndroidKeys(LayoutFileModel layoutFile,
String layoutName, File skinFilesPath) throws SkinException
{
// Retrieve a map of keycodes related to the keyboard declared at the layout
Collection<IAndroidKey> keyCollection = new LinkedHashSet<IAndroidKey>();
Properties keycodes = getKeycodes(layoutFile, skinFilesPath);
// Retrieve the names of the parts that compose the layout
Collection<String> partNames = layoutFile.getLayoutPartNames(layoutName);
// Gets the layout offset for position adjustments
Point offsetL = getLayoutOffset(layoutFile, layoutName, skinFilesPath);
// Iterates on the parts, looking for their buttons
for (String partName : partNames)
{
Point offsetP = getPartOffset(layoutFile, partName);
Point partSize = getPartImageSize(layoutFile, partName, skinFilesPath, offsetP);
// Iterates on the buttons of the part, creating an IAndroidKey for each in the end
Collection<String> buttonNames = layoutFile.getButtonNames(partName);
for (String buttonName : buttonNames)
{
String k = buttonName.toUpperCase().replace("-", "_");
String keyCodeStr = (String) keycodes.get(k);
// The IAndroidKey will only be generated if a keycode with the same name is found
if (keyCodeStr != null)
{
// Retrieve the button parameters needed for the key generation
// - The button position must be translated due to the layout and eventual offsets
// - The button w/h must be interpreted according to the rotation parameter. If the
// rotation is odd (landscape), the button width must be the key height and vice-versa.
// Otherwise (even, portrait), we can use the button w/h at the keys as is.
int buttonW = layoutFile.getButtonWidth(partName, buttonName, skinFilesPath);
int buttonH = layoutFile.getButtonHeight(partName, buttonName, skinFilesPath);
Point buttonPos =
translateButtonPosition(layoutFile, layoutName, partName, buttonName,
skinFilesPath, offsetP, partSize);
int startX = offsetL.x + buttonPos.x;
int startY = offsetL.y + buttonPos.y;
int endX, endY;
if (layoutFile.isSwapWidthHeightNeededAtLayout(layoutName, partName))
{
endX = startX + buttonH;
endY = startY + buttonW;
}
else
{
endX = startX + buttonW;
endY = startY + buttonH;
}
int dpadRotation = layoutFile.getDpadRotation(layoutName);
keyCodeStr = getRotatedKeyCode(k, keyCodeStr, dpadRotation, keycodes);
AndroidPressKey key =
new AndroidPressKey(buttonName, keyCodeStr, buttonName, startX, startY,
endX, endY, "", 0);
keyCollection.add(key);
}
}
}
return keyCollection;
}
/**
* @param keyCodeStr
* @param keyCodeStr2
* @param dpadRotation
* @param keycodes
* @return
*/
private static String getRotatedKeyCode(String keyName, String keyCodeStr, int dpadRotation,
Properties keycodes)
{
String keyCode = keyCodeStr;
switch (dpadRotation % 4)
{
case 1:
if (DPAD_DOWN.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_RIGHT);
}
else if (DPAD_LEFT.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_DOWN);
}
else if (DPAD_RIGHT.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_UP);
}
else if (DPAD_UP.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_LEFT);
}
break;
case 2:
if (DPAD_DOWN.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_UP);
}
else if (DPAD_LEFT.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_RIGHT);
}
else if (DPAD_RIGHT.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_LEFT);
}
else if (DPAD_UP.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_DOWN);
}
break;
case 3:
if (DPAD_DOWN.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_LEFT);
}
else if (DPAD_LEFT.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_UP);
}
else if (DPAD_RIGHT.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_DOWN);
}
else if (DPAD_UP.equals(keyName))
{
keyCode = (String) keycodes.get(DPAD_RIGHT);
}
break;
default:
//Does nothing, no rotation needed.
break;
}
return keyCode;
}
/**
* Merge two ImageData objects and return the merge.
* <br><br>
* @param srcData The source ImageData
* @param dstData The destination ImageData
* @param dstX The x position in dstData where srcData will be merged
* @param dstY The y position in dstData where srcData will be merged
* <br><br>
* @return An array containing released and pressed image data at the positions 0 and 1, respectively
*/
static ImageData mergeImageGC(ImageData srcData, ImageData dstData, int dstX, int dstY) {
Shell s = new Shell();
Image srcImg = new Image(s.getDisplay(), srcData);
Image dstImg = new Image(s.getDisplay(), dstData);
GC gc = new GC(dstImg);
gc.drawImage(srcImg, dstX, dstY);
gc.dispose();
return dstImg.getImageData();
}
/**
* Creates the layout images for released and pressed buttons
* <br><br>
* @param layoutFile The model that represents the layout file
* @param layoutName The name of the layout where to look for images in the model
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return An array containing released and pressed image data at the positions 0 and 1, respectively
*/
static ImageData[] generateLayoutImages(LayoutFileModel layoutFile, String layoutName,
File skinFilesPath)
{
ImageData[] layoutImgs = new ImageData[3];
// Gets the layout offset for position adjustments
Point offsetL = getLayoutOffset(layoutFile, layoutName, skinFilesPath);
// Iterates on the names of the parts that compose the layout
Collection<String> partNames = layoutFile.getLayoutPartNames(layoutName);
for (String partName : partNames)
{
if (!layoutFile.partHasBg(partName))
{
continue;
}
if (partName.equals("portrait") || partName.equals("landscape"))
{
if (!partName.equals(layoutName))
{
continue;
}
}
// Gets the part offset for position adjustments and images loading decision
Point offsetP = getPartOffset(layoutFile, partName);
Point partSize = getPartImageSize(layoutFile, partName, skinFilesPath, offsetP);
int bgH = layoutFile.getBackgroundHeight(partName, skinFilesPath);
int bgW = layoutFile.getBackgroundWidth(partName, skinFilesPath);
// Loads the part images for released and pressed. If there is a part offset, it is needed
// to generated an expanded part image data
ImageData[] bgDatas = new ImageData[3];
if ((offsetP.x > 0) || (offsetP.y > 0) || (partSize.x > offsetP.x + bgW)
|| (partSize.y > offsetP.y + bgH))
{
bgDatas[0] =
generateExpandedPartImageData(layoutFile, layoutName, partName, offsetP,
skinFilesPath, offsetP, partSize);
}
else
{
bgDatas[0] = getImageData(layoutFile, partName, null, skinFilesPath);
}
bgDatas[1] =
generateMergedWithButtonsImage(layoutFile, (ImageData) bgDatas[0].clone(),
partName, skinFilesPath, offsetP, false);
bgDatas[2] =
generateMergedWithButtonsImage(layoutFile, (ImageData) bgDatas[0].clone(),
partName, skinFilesPath, offsetP, true);
// Loop for generating layout images based on the part images above
for (int img = 0; img < 3; img++)
{
// A layout image is created only if it wasn't yet, no matter how many parts the layout has.
if (layoutImgs[img] == null)
{
Point layoutSize =
getLayoutImageSize(layoutFile, layoutName, skinFilesPath, offsetL);
layoutImgs[img] =
generateImageDataWithBackground(layoutFile, layoutName, layoutSize.x,
layoutSize.y, bgDatas[img], skinFilesPath);
}
// Copy the part image pixels into the layout image
// - Rotation units rotates the image in CLOCKWISE direction
// - The part position must be translated because:
// a) At Google layout file, the (x,y) position represents the upper left corner of the
// part image, no matter what is the rotation value
// b) We are generating a layout image referenced at the upper left corner of the layout
// image, and we must know where we must place the part image in terms of the layout reference
// - The part is rotated before merged to the layout image
int rotation = layoutFile.getPartRotationAtLayout(layoutName, partName);
Point partPos =
translatePartPosition(layoutFile, layoutName, partName, skinFilesPath,
offsetP, partSize);
bgDatas[img] = generateRotatedImage(bgDatas[img], rotation);
int startX = offsetL.x - offsetP.x + partPos.x;
int startY = offsetL.y - offsetP.y + partPos.y;
ImageData merge = mergeImageGC(bgDatas[img], layoutImgs[img], startX, startY);
layoutImgs[img] = merge;
}
}
return layoutImgs;
}
/**
* Translates the display information from the layout file into an Point referenced at the
* upper left corner of the layout image (or part image, if layouts are not supported by the skin)
* <br><br>
* @param layoutFile The model that represents the layout file
* @param layoutName The name of the layout being used
* @param partName The name of the part which contains the display
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return A point referenced at the upper left corner of the layout image, where to draw the display
*/
static Point translateDisplayPosition(LayoutFileModel layoutFile, String layoutName,
String partName, File skinFilesPath)
{
// Gets the parameters necessary for calculation
Point displayPos = layoutFile.getDisplayPosition(partName);
int displayW = layoutFile.getDisplayWidth(partName);
int displayH = layoutFile.getDisplayHeight(partName);
Point offsetP = getPartOffset(layoutFile, partName);
Point partSize;
if (!layoutFile.partHasBg(partName))
{
partSize = getPartImageSize(layoutFile, layoutName, skinFilesPath, offsetP);
}
else
{
partSize = getPartImageSize(layoutFile, partName, skinFilesPath, offsetP);
}
// Update the display position, considering part offset/size and rotation
displayPos =
translatePartElementPosition(layoutFile, layoutName, partName, skinFilesPath,
displayPos, displayW, displayH, offsetP, partSize);
// Adjusts the position (according to the layout offset) and returns to the caller
Point offsetL = getLayoutOffset(layoutFile, layoutName, skinFilesPath);
displayPos.x += offsetL.x;
displayPos.y += offsetL.y;
return displayPos;
}
/**
* Retrieves the keycodes to be used at the keys.
* Uses as parameter the keyboard charmap declared at the layout file model
* <br><br>
* @param layoutFile The model that represents the layout file
* <br><br>
* @return A map of properties containing the keycodes for each key of the charmap
* declared at the layout file
* <br><br>
* @throws SkinException If the keycode file cannot be loaded
*/
static Properties getKeycodes(LayoutFileModel layoutFile, File skinFilesPath)
throws SkinException
{
String charmap = layoutFile.getKeyboardCharmap();
Properties keycodes = new Properties();
InputStream is = null;
URL url = null;
try
{
// If nothing is specified, use the QWERTY charmap
// If it is specified QWERTY or QWERTY2, use QWERTY charmap too
if ((charmap == null) || (charmap.equals(CHARMAP_QWERTY))
|| (charmap.equals(CHARMAP_QWERTY2)))
{
url =
EmulatorPlugin
.getDefault()
.getBundle()
.getResource(
SdkUtils.isOphoneSDK() ? CHARMAP_OPHONE_QWERTY_FILE
: CHARMAP_QWERTY_FILE);
}
else if (charmap.equals(CHARMAP_AVRCP))
{
url =
EmulatorPlugin
.getDefault()
.getBundle()
.getResource(
SdkUtils.isOphoneSDK() ? CHARMAP_OPHONE_AVRCP_FILE
: CHARMAP_AVRCP_FILE);
}
else
{
warn("The skin at " + skinFilesPath.getAbsolutePath()
+ " does not use a supported charmap");
return keycodes;
}
is = url.openStream();
keycodes.load(is);
}
catch (IOException e)
{
error("There was an error reading the file " + url);
throw new SkinException(EmulatorNLS.ERR_AndroidSkinTranslator_ErrorReadingKeycodeFile);
}
finally
{
try
{
is.close();
}
catch (IOException e)
{
StudioLogger.error("Could not close input stream: ", e.getMessage()); //$NON-NLS-1$
}
}
return keycodes;
}
public static Properties getQwertyKeyMap()
{
Properties keycodes = new Properties();
URL url =
EmulatorPlugin
.getDefault()
.getBundle()
.getResource(
SdkUtils.isOphoneSDK() ? CHARMAP_OPHONE_QWERTY_FILE
: CHARMAP_QWERTY_FILE);
InputStream in;
try
{
in = url.openStream();
keycodes.load(in);
}
catch (IOException e)
{
error("There was an error reading the file " + url);
}
return keycodes;
}
/**
* Calculates and retrieves the layout offset.
* The offset is different from (0, 0) if any part has negative coordinates (x or y) according to the
* specification of the layout file
* <br><br>
* @param layoutFile The model that represents the layout file
* @param layoutName The layout that we wish to have the offset calculated
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return The layout offset
*/
private static Point getLayoutOffset(LayoutFileModel layoutFile, String layoutName,
File skinFilesPath)
{
int minX = 0;
int minY = 0;
Collection<String> partNames = layoutFile.getLayoutPartNames(layoutName);
for (String partName : partNames)
{
if (partName.equals("portrait") || partName.equals("landscape"))
{
if (!partName.equals(layoutName))
{
continue;
}
}
Point offsetP = getPartOffset(layoutFile, partName);
Point partSize;
if (layoutFile.partHasBg(partName))
{
partSize = getPartImageSize(layoutFile, partName, skinFilesPath, offsetP);
}
else
{
continue;
// partSize = getPartImageSize(layoutFile, layoutName, skinFilesPath, offsetP);
}
// The part position needs translation because its coordinates may change due to
// the buttons position (if the part offset is different from (0, 0))
Point partPos =
translatePartPosition(layoutFile, layoutName, partName, skinFilesPath, offsetP,
partSize);
if (partPos.x < minX)
{
minX = partPos.x;
}
if (partPos.y < minY)
{
minY = partPos.y;
}
}
Point layoutOffset = new Point(Math.abs(minX), Math.abs(minY));
return layoutOffset;
}
/**
* Calculates and retrieves a part offset.
* The offset is different from (0, 0) if any button that belong to the part has negative coordinates
* (x or y) according to the specification of the layout file
* <br><br>
* @param layoutFile The model that represents the layout file
* @param partName The part that we wish to have the offset calculated
* <br><br>
* @return The part offset
*/
private static Point getPartOffset(LayoutFileModel layoutFile, String partName)
{
int minX = 0;
int minY = 0;
Collection<String> buttonNames = layoutFile.getButtonNames(partName);
for (String buttonName : buttonNames)
{
Point buttonPos = layoutFile.getButtonPosition(partName, buttonName);
if (buttonPos.x < minX)
{
minX = buttonPos.x;
}
if (buttonPos.y < minY)
{
minY = buttonPos.y;
}
}
Point offset = new Point(Math.abs(minX), Math.abs(minY));
return offset;
}
/**
* Retrieves the size of the layout image.
* <br><br>
* @param layoutFile The model that represents the layout file
* @param layoutName The name of the layout to have its size calculated
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return The size of the layout image
*/
private static Point getLayoutImageSize(LayoutFileModel layoutFile, String layoutName,
File skinFilesPath, Point layoutOffset)
{
int maxX = 0;
int maxY = 0;
// Iterates on the layout parts, looking for the area required by each of them
Collection<String> partNames = layoutFile.getLayoutPartNames(layoutName);
for (String partName : partNames)
{
if (partName.equals("portrait") || partName.equals("landscape"))
{
if (!partName.equals(layoutName))
{
continue;
}
}
// The part position must be translated because:
// - At Google layout file, the part (x,y) position represents the upper left corner of the
// PART image, no matter what is the rotation value
// - We are generating a layout image referenced at the upper left corner of the LAYOUT
// image, and we must know where we must place the part image in terms of the layout reference
Point offsetP = getPartOffset(layoutFile, partName);
Point partSize = getPartImageSize(layoutFile, partName, skinFilesPath, offsetP);
Point partPos =
translatePartPosition(layoutFile, layoutName, partName, skinFilesPath, offsetP,
partSize);
if (layoutFile.isSwapWidthHeightNeededAtLayout(layoutName, partName))
{
// If the part is in landscape direction, we sum the width at y and
// height at x
//
// --------------
// Y | width |
// | |
// A | h | --------------------------
// X | e | | height |
// I | i | | w |
// S | g | | i |
// | h | | d |
// | t | | t |
// | | | h ROTATED PART |
// | PART | --------------------------
// --------------
// X AXIS
//
maxX = Math.max(maxX, layoutOffset.x + partPos.x + partSize.y);
maxY = Math.max(maxY, layoutOffset.y + partPos.y + partSize.x);
}
else
{
// If the part is in portrait direction, we sum the w/h as is
maxX = Math.max(maxX, layoutOffset.x + partPos.x + partSize.x);
maxY = Math.max(maxY, layoutOffset.y + partPos.y + partSize.y);
}
}
Point imgSize = new Point(maxX, maxY);
return imgSize;
}
/**
* Retrieves the minimum size of the part image.
* <br><br>
* @param layoutFile The model that represents the layout file
* @param partName The name of the part to have its size calculated
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return The minimum size of the part image
*/
private static Point getPartImageSize(LayoutFileModel layoutFile, String partName,
File skinFilesPath, Point partOffset)
{
// The initial maximum is set to be the width/height of the original image + part offset
//
// It will be the final part size IF there are no buttons with negative coordinates AND
// there are no buttons that is positioned in a coordinate close to the edge enough for not
// fitting (considering their width/height)
int bgWidth = layoutFile.getBackgroundWidth(partName, skinFilesPath);
int bgHeight = layoutFile.getBackgroundHeight(partName, skinFilesPath);
Point bgPos = layoutFile.getBackgroundPosition(partName);
int maxX = partOffset.x + bgPos.x + bgWidth;
int maxY = partOffset.y + bgPos.y + bgHeight;
// Iterates on the buttons, looking for the area required by each of them
Collection<String> buttonNames = layoutFile.getButtonNames(partName);
for (String buttonName : buttonNames)
{
int btWidth = layoutFile.getButtonWidth(partName, buttonName, skinFilesPath);
int btHeight = layoutFile.getButtonHeight(partName, buttonName, skinFilesPath);
Point buttonPos = layoutFile.getButtonPosition(partName, buttonName);
// If a button is described to be drawn outside the current maximum
// (x,y) position, update the (x,y) to make it fit
//
// -------------- --------------
// (x,y)| | | |
// --- | | |
// | | button | | (x,y) |
// --- with | | ----
// | negative | | | | button positioned in coordinates
// | coordinate| | | | inside the part, but with width
// | | | ---- large enough not to fit
// | | | |
// | | | |
// | PART | | PART |
// -------------- --------------
//
//
maxX = Math.max(maxX, partOffset.x + buttonPos.x + btWidth);
maxY = Math.max(maxY, partOffset.y + buttonPos.y + btHeight);
}
Point imgSize = new Point(maxX, maxY);
return imgSize;
}
/**
* Utility method for loading a image, given the model and part/button
* <br><br>
* @param layoutFile The model that represents the layout file
* @param partName The name of the part to have its background image loaded, or that contains the button
* @param buttonName The name of the button we wish the image loaded, or <code>null</code> if we aim to
* load the part background image
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return An image data object, containing the image pixels
*/
private static ImageData getImageData(LayoutFileModel layoutFile, String partName,
String buttonName, File skinFilesPath)
{
File f;
if (buttonName == null)
{
f = layoutFile.getBackgroundImage(partName, skinFilesPath);
}
else
{
f = new File(skinFilesPath, layoutFile.getButtonImage(partName, buttonName).getName());
}
return new ImageData(f.getAbsolutePath());
}
/**
* Creates a new expanded part image that contains enough space for drawing all the buttons
* <br><br>
* @param layoutFile The model that represents the layout file
* @param layoutName The name of the layout containing the part, if there is one, or <code>null</code>
* @param partName The part to be expanded
* @param offset The part offset to use when positioning the part image at the expanded image
* @param skinFilesPath The path to the skin dir, where skin files can be found
* <br><br>
* @return An image data containing the part image with more space at the background area
*/
private static ImageData generateExpandedPartImageData(LayoutFileModel layoutFile,
String layoutName, String partName, Point offset, File skinFilesPath, Point partOffset,
Point partSize)
{
ImageData img = null;
if (partName != null)
{
// Creates an image data with dimensions defined by partSize, and background color, depth
// and palette defined by bgImg
ImageData bgImg = getImageData(layoutFile, partName, null, skinFilesPath);
img =
generateImageDataWithBackground(layoutFile, layoutName, partSize.x, partSize.y,
bgImg, skinFilesPath);
// Merges the bgImg pixels at the image data
int[] row = new int[bgImg.width];
for (int i = 0; i < bgImg.height; i++)
{
bgImg.getPixels(0, i, bgImg.width, row, 0);
img.setPixels(offset.x, offset.y + i, bgImg.width, row, 0);
}
}
return img;
}
/**
* Creates an image data of dimensions x, y and the same background color as srcImg
* <br><br>
* @param layoutFile The model that represents the layout file
* @param layoutName The name of the layout that may have a background color defined
* @param width The width of the image
* @param height The height of the image
* @param srcImg An image that we wish to have its depth, palette (and perhaps background color)
* copied to the new image
* <br><br>
* @return An image of size (width, height), same depth and palette of srcImg and with filled with the
* background color defined by the model or srcImg
*/
private static ImageData generateImageDataWithBackground(LayoutFileModel layoutFile,
String layoutName, int width, int height, ImageData srcImg, File skinFilesPath)
{
ImageData img = new ImageData(width, height, srcImg.depth, srcImg.palette);
// Discover what is the background color. This is needed to fill the
// spaces around the layout image
RGB bgColor = layoutFile.getLayoutColor(layoutName, skinFilesPath);
int bgPixel = srcImg.palette.getPixel(bgColor);
// Set the background color to the entire layout image
for (int i = 0; i < img.width; i++)
{
for (int j = 0; j < img.height; j++)
{
img.setPixel(i, j, bgPixel);
}
}
return img;
}
/**
* Creates a new image, based on imageToRotate, rotated according to rotation
* <br><br>
* @param imageToRotate The image that is used as source for rotation
* @param rotation (rotation * 90) results in how many degrees to rotate CLOCKWISE
* <br><br>
* @return The rotated image
*/
private static ImageData generateRotatedImage(ImageData imageToRotate, int rotation)
{
// For each rotation case, generates an appropriate image data (with dimensions w/h or h/w
// depending on whether it is landscape or portrait) and copies the imageToRotate pixels at the
// appropriate positions
ImageData rotated;
switch (rotation % 4)
{
case 1:
// 0------- 0 j h
// | --- | ---------
// j| | | | | --- |
// | --- | | | | |
// | | | --- |
// h------- ---------
rotated =
new ImageData(imageToRotate.height, imageToRotate.width,
imageToRotate.depth, imageToRotate.palette);
for (int i = 0; i < imageToRotate.width; i++)
{
for (int j = 0; j < imageToRotate.height; j++)
{
rotated.setPixel(imageToRotate.height - j - 1, i,
imageToRotate.getPixel(i, j));
}
}
break;
case 2:
// 0 i w 0 i w
// 0------- 0-------
// | --- | | |
// j| | | | j| --- |
// | --- | | | | |
// | | | --- |
// h------- h-------
rotated =
new ImageData(imageToRotate.width, imageToRotate.height,
imageToRotate.depth, imageToRotate.palette);
for (int i = 0; i < imageToRotate.width; i++)
{
for (int j = 0; j < imageToRotate.height; j++)
{
rotated.setPixel(imageToRotate.width - i - 1, imageToRotate.height - j - 1,
imageToRotate.getPixel(i, j));
}
}
break;
case 3:
// 0 i w
// 0------- 0 j h
// | --- | 0---------
// j| | | | | --- |
// | --- | i| | | |
// | | | --- |
// h------- w---------
rotated =
new ImageData(imageToRotate.height, imageToRotate.width,
imageToRotate.depth, imageToRotate.palette);
for (int i = 0; i < imageToRotate.width; i++)
{
for (int j = 0; j < imageToRotate.height; j++)
{
rotated.setPixel(j, imageToRotate.width - i - 1,
imageToRotate.getPixel(i, j));
}
}
break;
default:
// If 0, there is no need to rotate
rotated = imageToRotate;
break;
}
return rotated;
}
/**
* Creates an image that contains buttons with proper transparency. The transparency is defined by
* the isEnter parameter
*
* @param layoutFile The model that represents the layout file
* @param baseImage The image to use as base for generation. Must be a copy, because it will be
* changed in-place.
* @param partName The name of the part where to look for buttons in the model
* @param skinFilesPath The path to the skin dir, where skin files can be found
* @param partOffset What is the calculated offset for the given part
* @param isEnter Whether the image being created will be used for enter or pressed
* @return
*/
private static ImageData generateMergedWithButtonsImage(LayoutFileModel layoutFile,
ImageData baseImage, String partName, File skinFilesPath, Point partOffset,
boolean isEnter)
{
// Iterate on the buttons, merging the buttons pixels to the base image
Collection<String> buttonNames = layoutFile.getButtonNames(partName);
for (String buttonName : buttonNames)
{
ImageData buttonID = getImageData(layoutFile, partName, buttonName, skinFilesPath);
Point buttonPos = layoutFile.getButtonPosition(partName, buttonName);
buttonPos.x += partOffset.x;
buttonPos.y += partOffset.y;
mergeButtonData(baseImage, buttonID, buttonPos, isEnter);
}
return baseImage;
}
/**
* Merges the button data to the base image
* <br><br>
* @param baseImage The image that will be modified
* @param buttonImage The image that have the source pixels for the merge operation
* @param buttonPos Where the button is located
* @param isEnter Whether the image being created will be used for enter or pressed
*/
private static void mergeButtonData(ImageData baseImage, ImageData buttonImage,
Point buttonPos, boolean isEnter)
{
// Pixel/alpha buffers
int[] baseImgPixels = new int[buttonImage.width];
int[] buttonImgPixels = new int[buttonImage.width];
byte[] buttonAlphas = new byte[buttonImage.width];
int[] intButtonAlphas = new int[buttonImage.width];
// For each pixel row, get the button pixel data, apply the transparency
// defined by alpha and copy data to the base image
for (int i = 0; i < buttonImage.height; i++)
{
baseImage.getPixels(buttonPos.x, buttonPos.y + i, buttonImage.width, baseImgPixels, 0);
buttonImage.getPixels(0, i, buttonImage.width, buttonImgPixels, 0);
buttonImage.getAlphas(0, i, buttonImage.width, buttonAlphas, 0);
for (int j = 0; j < buttonAlphas.length; j++)
{
// As buttonAlphas is a signed byte array with range -127 to 128, and alpha is
// an integer in the range 0 to 255, overflows can happen. This calculation assures
// that the alpha variable has correct value in an integer array.
intButtonAlphas[j] =
(buttonAlphas[j] >= 0 ? buttonAlphas[j]
: ((buttonAlphas[j]) & ((byte) 0x7F)) + 128);
}
if (!isEnter)
{
for (int j = 0; j < buttonAlphas.length; j++)
{
if (intButtonAlphas[j] > 0)
{
intButtonAlphas[j] += (255 - intButtonAlphas[j]) / 4;
}
}
}
addTransparency(baseImgPixels, buttonImgPixels, intButtonAlphas, baseImage.palette,
buttonImage.palette);
baseImage.setPixels(buttonPos.x, buttonPos.y + i, buttonImage.width, baseImgPixels, 0);
}
}
/**
* Calculates transparency for the button pixels and sets them to the base
* pixels buffer
* <br><br>
* @param basePixels The buffer containing pixels for a given line of the base image
* @param buttonPixels The buffer containing pixels for a given line of the button image
* @param buttonAlphas The buffer containing alpha information for a given line of the button image
* @param basePalette The color palette used by the base image
* @param buttonPalette The color palette used by the button image
*/
private static void addTransparency(int[] basePixels, int[] buttonPixels, int[] buttonAlphas,
PaletteData basePalette, PaletteData buttonPalette)
{
for (int i = 0; i < buttonPixels.length; i++)
{
RGB buttonRgb = buttonPalette.getRGB(buttonPixels[i]);
RGB baseRgb = basePalette.getRGB(basePixels[i]);
RGB newRgb =
new RGB(calculateMerge(baseRgb.red, buttonRgb.red, buttonAlphas[i]),
calculateMerge(baseRgb.green, buttonRgb.green, buttonAlphas[i]),
calculateMerge(baseRgb.blue, buttonRgb.blue, buttonAlphas[i]));
basePixels[i] = basePalette.getPixel(newRgb);
}
}
/**
* Calculates the transparency for a single color component
* <br><br>
* @param background The background color component
* @param foreground The foreground color component
* @param alpha The alpha to be applied. 0 means pure transparent
* (background color is used). 255 means pure opaque (foreground color is used)
* <br><br>
* @return The resulting color
*/
private static int calculateMerge(int background, int foreground, int alpha)
{
// weighted medium of foreground color and background color, with alpha as parameter
return (foreground * alpha + background * (255 - alpha)) / 255;
}
/**
* Translates the part position information from the Google format to the upper-left reference used
* by the viewer
*
* @param layoutFile The model that represents the layout file
* @param layoutName The layout where the part is included
* @param partName The part to have its position calculated
* @param skinFilesPath The path to the skin dir, where skin files can be found
*
* @return The point where the part must be drawn in the layout, using as reference the upper-left
* corner of the layout image
*/
private static Point translatePartPosition(LayoutFileModel layoutFile, String layoutName,
String partName, File skinFilesPath, Point partOffset, Point partSize)
{
// Collect needed data
int rotation = layoutFile.getPartRotationAtLayout(layoutName, partName);
Point partPos = layoutFile.getPartPositionAtLayout(layoutName, partName, skinFilesPath);
int bgWidth;
int bgHeight;
if (layoutFile.partHasBg(partName))
{
bgWidth = layoutFile.getBackgroundWidth(partName, skinFilesPath);
bgHeight = layoutFile.getBackgroundHeight(partName, skinFilesPath);
}
else
{
bgWidth = layoutFile.getBackgroundWidth(layoutName, skinFilesPath);
bgHeight = layoutFile.getBackgroundHeight(layoutName, skinFilesPath);
}
int extraOnEndW = partSize.x - bgWidth - partOffset.x;
int extraOnEndH = partSize.y - bgHeight - partOffset.y;
// Calculate translation
switch (rotation % 4)
{
case 1:
// Landscape, top of part image is at the right (90 degrees clockwise rotation)
// The point we must return is the one at the bottom-left corner of the part, considering
// offset and extra space in the end of the part image (which was added so that buttons
// at the right side of the part fit)
//
// BEFORE AFTER
// (0,0) (0,0)
// --------- ---------
// | --- | | --- |
// | | | | | | | |
// | --- | | --- |
// --------- ---------
partPos.x = partPos.x - partOffset.y - bgHeight;
partPos.y = partPos.y - extraOnEndW;
break;
case 2:
// Portrait, top of part image is at the bottom (180 degrees clockwise rotation)
// The point we must return is the one at the bottom-right corner of the part
//
// BEFORE AFTER
// (0,0)
// ------- -------
// | | | |
// | --- | | --- |
// | | | | | | | |
// | --- | | --- |
// ------- -------
// (0,0)
partPos.x = partPos.x - bgWidth;
partPos.y = partPos.y - bgHeight;
break;
case 3:
// Landscape, top of part image is at the left (270 degrees clockwise rotation)
// The point we must return is the one at the top-right corner of the part, considering
// offset and extra space in the end of the part image (which was added so that buttons
// at the right side of the part fit)
//
// BEFORE AFTER
// (0,0)
// --------- ---------
// | --- | | --- |
// | | | | | | | |
// | --- | | --- |
// --------- ---------
//(0,0)
partPos.x = partPos.x - extraOnEndH;
partPos.y = partPos.y - partOffset.x - bgWidth;
break;
default:
// No translation is needed when there is no rotation
break;
}
return partPos;
}
/**
* Translates the button position information from the Google format to the upper-left reference used
* by the viewer
*
* @param layoutFile The model that represents the layout file
* @param layoutName The layout where the part is included, or <code>null</code> if the skin does
* not support layout
* @param partName The part where the button is included
* @param buttonName The button to have its position calculated
* @param skinFilesPath The path to the skin dir, where skin files can be found
*
* @return The point where the button must be drawn in the part, using as reference the upper-left
* corner of the part image
*/
private static Point translateButtonPosition(LayoutFileModel layoutFile, String layoutName,
String partName, String buttonName, File skinFilesPath, Point partOffset, Point partSize)
{
// Collect button data
Point buttonPos = layoutFile.getButtonPosition(partName, buttonName);
int buttonW = layoutFile.getButtonWidth(partName, buttonName, skinFilesPath);
int buttonH = layoutFile.getButtonHeight(partName, buttonName, skinFilesPath);
// Update the button position, considering part offset/size and rotation
buttonPos =
translatePartElementPosition(layoutFile, layoutName, partName, skinFilesPath,
buttonPos, buttonW, buttonH, partOffset, partSize);
return buttonPos;
}
/**
* Translates a part element position (display/buttons) from the Google format to the upper-left
* reference used by the viewer
*
* @param layoutFile The model that represents the layout file
* @param layoutName The layout where the part is included, or <code>null</code> if the skin does
* not support layout
* @param partName The part where the element is included
* @param skinFilesPath The path to the skin dir, where skin files can be found
* @param partElementPos The position of the part element as described by layoutFile
* @param partElementWidth The width of the part element as described by layoutFile
* @param partElementHeight The height of the part element as described by layoutFile
*
* @return The point where the element must be drawn in the part, using as reference the upper-left
* corner of the part image
*/
private static Point translatePartElementPosition(LayoutFileModel layoutFile,
String layoutName, String partName, File skinFilesPath, Point partElementPos,
int partElementWidth, int partElementHeight, Point partOffset, Point partSize)
{
Point translated = new Point(0, 0);
int rotation = layoutFile.getPartRotationAtLayout(layoutName, partName);
// Due to rotation, the part position will be referenced to a non-appropriate image corner.
// The following operation guarantees that the part position is still at the upper left corner
// even after rotation.
Point partPos =
translatePartPosition(layoutFile, layoutName, partName, skinFilesPath, partOffset,
partSize);
// Calculate position.
//
// OBS: Every time we need the part size for our the calculation, we must subtract the part offset
// as well. This is because during part size calculation, we have already summed the offset and we
// need to rework the offset due to rotation (i.e., sometimes we need to sum offset.y instead of
// offset.x due to rotation, and vice-versa). This is being illustrated at the lines below with
// parenthesis.
switch (rotation % 4)
{
case 1:
// BEFORE AFTER
//(0,0)
// --------- (0,0)
// | | -----------
// |(x,y) | (x,y) |
// | --- | | --- |
// | | | | | | | |
// | --- | | --- |
// --------- -----------
translated.x =
partPos.x - partOffset.x + (partSize.y - partOffset.y) - partElementPos.y
- partElementHeight;
translated.y = partPos.y - partOffset.y + partOffset.x + partElementPos.x;
break;
case 2:
// BEFORE AFTER
//(0,0) (0,0)
// --------- ---------
// | | (x,y) --- |
// |(x,y) | | | | |
// | --- | | --- |
// | | | | | |
// | --- | | |
// --------- ---------
translated.x =
partPos.x + (partSize.x - partOffset.x) - partOffset.x - partElementPos.x
- partElementWidth;
translated.y =
partPos.y + (partSize.y - partOffset.y) - partOffset.y - partElementPos.y
- partElementHeight;
break;
case 3:
// BEFORE AFTER
//(0,0)
// --------- (0,0)
// | | -----------
// |(x,y) | |(x,y)--- |
// | --- | | | | |
// | | | | | --- |
// | --- | | |
// --------- -----------
translated.x = partPos.x - partOffset.x + partOffset.y + partElementPos.y;
translated.y =
partPos.y - partOffset.y + (partSize.x - partOffset.x) - partElementPos.x
- partElementWidth;
break;
default:
translated.x = partElementPos.x + partPos.x;
translated.y = partElementPos.y + partPos.y;
break;
}
return translated;
}
}