diff options
Diffstat (limited to 'hierarchyviewer/src/com/android/hierarchyviewer')
39 files changed, 5776 insertions, 0 deletions
diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java b/hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java new file mode 100644 index 000000000..385ee793c --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.Window; +import com.android.hierarchyviewer.scene.CaptureLoader; +import com.android.hierarchyviewer.ui.Workspace; +import com.android.hierarchyviewer.device.DeviceBridge; + +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import java.io.File; + +public class HierarchyViewer { + private static final CharSequence OS_WINDOWS = "Windows"; + private static final CharSequence OS_MACOSX = "Mac OS X"; + + private static boolean sProfilingEnabled = true; + + public static boolean isProfilingEnabled() { + return sProfilingEnabled; + } + + private static void initUserInterface() { + System.setProperty("apple.laf.useScreenMenuBar", "true"); + System.setProperty("apple.awt.brushMetalLook", "true"); + System.setProperty("com.apple.mrj.application.apple.menu.about.name", "HierarchyViewer"); + + final String os = System.getProperty("os.name"); + + try { + if (os.contains(OS_WINDOWS) || os.contains(OS_MACOSX)) { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } else { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } + } + + private static void listDevices() { + System.out.println("List of devices attached"); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + for (IDevice device : DeviceBridge.getDevices()) { + printDevice(device); + } + DeviceBridge.terminate(); + } + + private static void printDevice(IDevice device) { + System.out.println(device.toString() + "\t\t" + + (device.isEmulator() ? "emulator" : "device")); + } + + private static void outputPsd(String deviceName, String file) { + IDevice device = selectDevice(deviceName); + if (device != null) { + if (DeviceBridge.isViewServerRunning(device)) { + DeviceBridge.stopViewServer(device); + } + DeviceBridge.startViewServer(device); + DeviceBridge.setupDeviceForward(device); + System.out.println("Capturing layers to " + file); + CaptureLoader.saveLayers(device, Window.FOCUSED_WINDOW, new File(file)); + } else { + System.out.println("The selected device does not exist"); + } + DeviceBridge.terminate(); + } + + private static IDevice selectDevice(String deviceName) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (DeviceBridge.getDevices() == null) return null; + if (deviceName == null) return DeviceBridge.getDevices()[0]; + for (IDevice device : DeviceBridge.getDevices()) { + if (device.getSerialNumber().equalsIgnoreCase(deviceName)) { + return device; + } + } + return null; + } + + public static void main(String[] args) { + DeviceBridge.initDebugBridge(); + + if (args.length > 0) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if ("--help".equalsIgnoreCase(arg)) { + System.out.println("Usage: hierarchyviewer1 [options]\n"); + System.out.println("Options:"); + System.out.println(" --help\t\t\t Show this help message and exit"); + System.out.println(" --no-profiling\t Disable views profiling"); + System.out.println(" --devices\t\t\t Show the list of available devices"); + System.out.println(" --psd [device] <file>\t Export psd and exit"); + System.exit(0); + } else if ("--no-profiling".equalsIgnoreCase(arg)) { + sProfilingEnabled = false; + } else if ("--devices".equalsIgnoreCase(arg)) { + listDevices(); + System.exit(0); + } else if ("--psd".equalsIgnoreCase(arg)) { + if (i == args.length - 1) { + System.out.println("You must specify at least an output file with --psd"); + System.exit(1); + } + String device = null; + String file = null; + if (i < args.length - 2) { + device = args[++i]; + } + if (i < args.length - 1) { + file = args[++i]; + } + outputPsd(device, file); + System.exit(0); + } + } + } + + initUserInterface(); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + Workspace workspace = new Workspace(); + workspace.setDefaultCloseOperation(Workspace.EXIT_ON_CLOSE); + workspace.setLocationRelativeTo(null); + workspace.setVisible(true); + } + }); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/device/Configuration.java b/hierarchyviewer/src/com/android/hierarchyviewer/device/Configuration.java new file mode 100644 index 000000000..090730f1c --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/device/Configuration.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.device; + +public class Configuration { + public static final int DEFAULT_SERVER_PORT = 4939; + + // These codes must match the auto-generated codes in IWindowManager.java + // See IWindowManager.aidl as well + public static final int SERVICE_CODE_START_SERVER = 1; + public static final int SERVICE_CODE_STOP_SERVER = 2; + public static final int SERVICE_CODE_IS_SERVER_RUNNING = 3; +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/device/DeviceBridge.java b/hierarchyviewer/src/com/android/hierarchyviewer/device/DeviceBridge.java new file mode 100644 index 000000000..f585ea66f --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/device/DeviceBridge.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.device; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.hierarchyviewer.scene.VersionLoader; + +import java.io.IOException; +import java.io.File; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DeviceBridge { + private static AndroidDebugBridge bridge; + + private static final HashMap<IDevice, Integer> devicePortMap = new HashMap<IDevice, Integer>(); + private static int nextLocalPort = Configuration.DEFAULT_SERVER_PORT; + + public static void initDebugBridge() { + if (bridge == null) { + AndroidDebugBridge.init(false /* debugger support */); + } + if (bridge == null || !bridge.isConnected()) { + String adbLocation = System.getProperty("hierarchyviewer.adb"); + if (adbLocation != null && adbLocation.length() != 0) { + adbLocation += File.separator + "adb"; + } else { + adbLocation = "adb"; + } + + bridge = AndroidDebugBridge.createBridge(adbLocation, true); + } + } + + public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { + AndroidDebugBridge.addDeviceChangeListener(listener); + } + + public static IDevice[] getDevices() { + return bridge.getDevices(); + } + + public static boolean isViewServerRunning(IDevice device) { + initDebugBridge(); + final boolean[] result = new boolean[1]; + try { + if (device.isOnline()) { + device.executeShellCommand(buildIsServerRunningShellCommand(), + new BooleanResultReader(result)); + if (!result[0]) { + if (VersionLoader.loadProtocolVersion(device) > 2) { + result[0] = true; + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } + return result[0]; + } + + public static boolean startViewServer(IDevice device) { + return startViewServer(device, Configuration.DEFAULT_SERVER_PORT); + } + + public static boolean startViewServer(IDevice device, int port) { + initDebugBridge(); + final boolean[] result = new boolean[1]; + try { + if (device.isOnline()) { + device.executeShellCommand(buildStartServerShellCommand(port), + new BooleanResultReader(result)); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } + return result[0]; + } + + public static boolean stopViewServer(IDevice device) { + initDebugBridge(); + final boolean[] result = new boolean[1]; + try { + if (device.isOnline()) { + device.executeShellCommand(buildStopServerShellCommand(), + new BooleanResultReader(result)); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } + return result[0]; + } + + public static void terminate() { + AndroidDebugBridge.terminate(); + } + + /** + * Sets up a just-connected device to work with the view server. + * <p/>This starts a port forwarding between a local port and a port on the device. + * @param device + */ + public static void setupDeviceForward(IDevice device) { + synchronized (devicePortMap) { + if (device.getState() == IDevice.DeviceState.ONLINE) { + int localPort = nextLocalPort++; + try { + device.createForward(localPort, Configuration.DEFAULT_SERVER_PORT); + devicePortMap.put(device, localPort); + } catch (TimeoutException e) { + Log.e("hierarchy", "Timeout setting up port forwarding for " + device); + } catch (AdbCommandRejectedException e) { + Log.e("hierarchy", String.format( + "Adb rejected forward command for device %1$s: %2$s", + device, e.getMessage())); + } catch (IOException e) { + Log.e("hierarchy", String.format( + "Failed to create forward for device %1$s: %2$s", + device, e.getMessage())); + } + } + } + } + + public static void removeDeviceForward(IDevice device) { + synchronized (devicePortMap) { + final Integer localPort = devicePortMap.get(device); + if (localPort != null) { + try { + device.removeForward(localPort, Configuration.DEFAULT_SERVER_PORT); + devicePortMap.remove(device); + } catch (TimeoutException e) { + Log.e("hierarchy", "Timeout removing port forwarding for " + device); + } catch (AdbCommandRejectedException e) { + Log.e("hierarchy", String.format( + "Adb rejected remove-forward command for device %1$s: %2$s", + device, e.getMessage())); + } catch (IOException e) { + Log.e("hierarchy", String.format( + "Failed to remove forward for device %1$s: %2$s", + device, e.getMessage())); + } + } + } + } + + public static int getDeviceLocalPort(IDevice device) { + synchronized (devicePortMap) { + Integer port = devicePortMap.get(device); + if (port != null) { + return port; + } + + Log.e("hierarchy", "Missing forwarded port for " + device.getSerialNumber()); + return -1; + } + + } + + private static String buildStartServerShellCommand(int port) { + return String.format("service call window %d i32 %d", + Configuration.SERVICE_CODE_START_SERVER, port); + } + + private static String buildStopServerShellCommand() { + return String.format("service call window %d", Configuration.SERVICE_CODE_STOP_SERVER); + } + + private static String buildIsServerRunningShellCommand() { + return String.format("service call window %d", + Configuration.SERVICE_CODE_IS_SERVER_RUNNING); + } + + private static class BooleanResultReader extends MultiLineReceiver { + private final boolean[] mResult; + + public BooleanResultReader(boolean[] result) { + mResult = result; + } + + @Override + public void processNewLines(String[] strings) { + if (strings.length > 0) { + Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); + Matcher matcher = pattern.matcher(strings[0]); + if (matcher.matches()) { + if (Integer.parseInt(matcher.group(1)) == 1) { + mResult[0] = true; + } + } + } + } + + public boolean isCancelled() { + return false; + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/device/Window.java b/hierarchyviewer/src/com/android/hierarchyviewer/device/Window.java new file mode 100644 index 000000000..5c87d3326 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/device/Window.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.device; + +public class Window { + public static final Window FOCUSED_WINDOW = new Window("<Focused Window>", -1); + + private String title; + private int hashCode; + + public Window(String title, int hashCode) { + this.title = title; + this.hashCode = hashCode; + } + + public String getTitle() { + return title; + } + + public int getHashCode() { + return hashCode; + } + + public String encode() { + return Integer.toHexString(hashCode); + } + + @Override + public String toString() { + return title; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/laf/UnifiedContentBorder.java b/hierarchyviewer/src/com/android/hierarchyviewer/laf/UnifiedContentBorder.java new file mode 100644 index 000000000..873b2755e --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/laf/UnifiedContentBorder.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.laf; + +import javax.swing.border.AbstractBorder; +import java.awt.*; + +public class UnifiedContentBorder extends AbstractBorder { + private static final Color BORDER_TOP_COLOR1 = new Color(0x575757); + private static final Color BORDER_BOTTOM_COLOR1 = new Color(0x404040); + private static final Color BORDER_BOTTOM_COLOR2 = new Color(0xd8d8d8); + + @Override + public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { + g.setColor(BORDER_TOP_COLOR1); + g.drawLine(x, y, x + width, y); + g.setColor(BORDER_BOTTOM_COLOR1); + g.drawLine(x, y + height - 2, x + width, y + height - 2); + g.setColor(BORDER_BOTTOM_COLOR2); + g.drawLine(x, y + height - 1, x + width, y + height - 1); + } + + @Override + public Insets getBorderInsets(Component component) { + return new Insets(1, 0, 2, 0); + } + + @Override + public boolean isBorderOpaque() { + return true; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/CaptureLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/CaptureLoader.java new file mode 100644 index 000000000..ca51b4e42 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/CaptureLoader.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.scene; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.Window; +import com.android.hierarchyviewer.device.DeviceBridge; +import com.android.hierarchyviewer.ui.util.PsdFile; + +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import javax.imageio.ImageIO; + +public class CaptureLoader { + public static boolean saveLayers(IDevice device, Window window, File file) { + Socket socket = null; + DataInputStream in = null; + BufferedWriter out = null; + boolean result = false; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new DataInputStream(new BufferedInputStream(socket.getInputStream())); + + out.write("CAPTURE_LAYERS " + window.encode()); + out.newLine(); + out.flush(); + + int width = in.readInt(); + int height = in.readInt(); + + PsdFile psd = new PsdFile(width, height); + + while (readLayer(in, psd)) { + } + + psd.write(new FileOutputStream(file)); + + result = true; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + return result; + } + + private static boolean readLayer(DataInputStream in, PsdFile psd) { + try { + if (in.read() == 2) { + System.out.println("Found end of layers list"); + return false; + } + String name = in.readUTF(); + System.out.println("name = " + name); + boolean visible = in.read() == 1; + int x = in.readInt(); + int y = in.readInt(); + int dataSize = in.readInt(); + + byte[] data = new byte[dataSize]; + int read = 0; + while (read < dataSize) { + read += in.read(data, read, dataSize - read); + } + + ByteArrayInputStream arrayIn = new ByteArrayInputStream(data); + BufferedImage chunk = ImageIO.read(arrayIn); + + // Ensure the image is in the right format + BufferedImage image = new BufferedImage(chunk.getWidth(), chunk.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.drawImage(chunk, null, 0, 0); + g.dispose(); + + psd.addLayer(name, image, new Point(x, y), visible); + + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public static Image loadCapture(IDevice device, Window window, String params) { + Socket socket = null; + BufferedInputStream in = null; + BufferedWriter out = null; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new BufferedInputStream(socket.getInputStream()); + + out.write("CAPTURE " + window.encode() + " " + params); + out.newLine(); + out.flush(); + + return ImageIO.read(in); + } catch (IOException e) { + // Empty + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + return null; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java new file mode 100644 index 000000000..4b9d524b0 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ProfilesLoader.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2009 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.android.hierarchyviewer.scene; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.HierarchyViewer; +import com.android.hierarchyviewer.device.Window; +import com.android.hierarchyviewer.device.DeviceBridge; + +import java.net.Socket; +import java.net.InetSocketAddress; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; + +public class ProfilesLoader { + public static double[] loadProfiles(IDevice device, Window window, String params) { + if (!HierarchyViewer.isProfilingEnabled()) { + return new double[] { 0.0, 0.0, 0.0 }; + } + + Socket socket = null; + BufferedReader in = null; + BufferedWriter out = null; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + out.write("PROFILE " + window.encode() + " " + params); + out.newLine(); + out.flush(); + + String response = in.readLine(); + String[] data = response.split(" "); + + double[] profiles = new double[data.length]; + for (int i = 0; i < data.length; i++) { + profiles[i] = (Long.parseLong(data[i]) / 1000.0) / 1000.0; // convert to ms + } + return profiles; + } catch (IOException e) { + // Empty + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + return null; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/VersionLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/VersionLoader.java new file mode 100644 index 000000000..ccc187d9b --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/VersionLoader.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009 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.android.hierarchyviewer.scene; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.DeviceBridge; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; + +public class VersionLoader { + public static int loadServerVersion(IDevice device) { + return loadVersion(device, "SERVER"); + } + + public static int loadProtocolVersion(IDevice device) { + return loadVersion(device, "PROTOCOL"); + } + + private static int loadVersion(IDevice device, String command) { + Socket socket = null; + BufferedReader in = null; + BufferedWriter out = null; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + out.write(command); + out.newLine(); + out.flush(); + + return Integer.parseInt(in.readLine()); + } catch (Exception e) { + // Empty + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + // Versioning of the protocol and server was added with version 2 + return 2; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java new file mode 100644 index 000000000..c9cb66d93 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.scene; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.DeviceBridge; +import com.android.hierarchyviewer.device.Window; + +import org.openide.util.Exceptions; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Collections; +import java.util.Comparator; +import java.util.Stack; + +public class ViewHierarchyLoader { + @SuppressWarnings("empty-statement") + public static ViewHierarchyScene loadScene(IDevice device, Window window) { + ViewHierarchyScene scene = new ViewHierarchyScene(); + + // Read the views tree + Socket socket = null; + BufferedReader in = null; + BufferedWriter out = null; + + String line; + + try { + System.out.println("==> Starting client"); + + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "utf-8")); + + System.out.println("==> DUMP"); + + out.write("DUMP " + window.encode()); + out.newLine(); + out.flush(); + + Stack<ViewNode> stack = new Stack<ViewNode>(); + + boolean setRoot = true; + ViewNode lastNode = null; + int lastWhitespaceCount = Integer.MAX_VALUE; + + while ((line = in.readLine()) != null) { + if ("DONE.".equalsIgnoreCase(line)) { + break; + } + + int whitespaceCount = countFrontWhitespace(line); + if (lastWhitespaceCount < whitespaceCount) { + stack.push(lastNode); + } else if (!stack.isEmpty()) { + final int count = lastWhitespaceCount - whitespaceCount; + for (int i = 0; i < count; i++) { + stack.pop(); + } + } + + lastWhitespaceCount = whitespaceCount; + line = line.trim(); + int index = line.indexOf(' '); + + lastNode = new ViewNode(); + lastNode.name = line.substring(0, index); + + line = line.substring(index + 1); + loadProperties(lastNode, line); + + scene.addNode(lastNode); + + if (setRoot) { + scene.setRoot(lastNode); + setRoot = false; + } + + if (!stack.isEmpty()) { + final ViewNode parent = stack.peek(); + final String edge = parent.name + lastNode.name; + scene.addEdge(edge); + scene.setEdgeSource(edge, parent); + scene.setEdgeTarget(edge, lastNode); + lastNode.parent = parent; + parent.children.add(lastNode); + } + } + + updateIndices(scene.getRoot()); + + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + socket.close(); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + } + + System.out.println("==> DONE"); + + return scene; + } + + private static void updateIndices(ViewNode root) { + if (root == null) return; + + root.computeIndex(); + + for (ViewNode node : root.children) { + updateIndices(node); + } + } + + private static int countFrontWhitespace(String line) { + int count = 0; + while (line.charAt(count) == ' ') { + count++; + } + return count; + } + + private static void loadProperties(ViewNode node, String data) { + int start = 0; + boolean stop; + + do { + int index = data.indexOf('=', start); + ViewNode.Property property = new ViewNode.Property(); + property.name = data.substring(start, index); + + int colonIndex = property.name.indexOf(':'); + if (colonIndex != -1) { + property.name = property.name.substring(colonIndex + 1); + } + + int index2 = data.indexOf(',', index + 1); + int length = Integer.parseInt(data.substring(index + 1, index2)); + start = index2 + 1 + length; + property.value = data.substring(index2 + 1, index2 + 1 + length); + + node.properties.add(property); + node.namedProperties.put(property.name, property); + + stop = start >= data.length(); + if (!stop) { + start += 1; + } + } while (!stop); + + Collections.sort(node.properties, new Comparator<ViewNode.Property>() { + public int compare(ViewNode.Property source, ViewNode.Property destination) { + return source.name.compareTo(destination.name); + } + }); + + node.decode(); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java new file mode 100644 index 000000000..08dc39590 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.scene; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.Point2D; + +import org.netbeans.api.visual.action.ActionFactory; +import org.netbeans.api.visual.action.WidgetAction; +import org.netbeans.api.visual.anchor.AnchorFactory; +import org.netbeans.api.visual.border.BorderFactory; +import org.netbeans.api.visual.graph.GraphScene; +import org.netbeans.api.visual.layout.LayoutFactory; +import org.netbeans.api.visual.model.ObjectState; +import org.netbeans.api.visual.widget.ConnectionWidget; +import org.netbeans.api.visual.widget.LabelWidget; +import org.netbeans.api.visual.widget.LayerWidget; +import org.netbeans.api.visual.widget.Widget; + +public class ViewHierarchyScene extends GraphScene<ViewNode, String> { + private ViewNode root; + private LayerWidget widgetLayer; + private LayerWidget connectionLayer; + + private WidgetAction moveAction = ActionFactory.createMoveAction(); + + public ViewHierarchyScene() { + widgetLayer = new LayerWidget(this); + connectionLayer = new LayerWidget(this); + + addChild(widgetLayer); + addChild(connectionLayer); + } + + public ViewNode getRoot() { + return root; + } + + void setRoot(ViewNode root) { + this.root = root; + } + + @Override + protected Widget attachNodeWidget(ViewNode node) { + Widget widget = createBox(node, node.name, node.id); + widget.getActions().addAction(createSelectAction()); + widget.getActions().addAction(moveAction); + widgetLayer.addChild(widget); + return widget; + } + + private Widget createBox(ViewNode node, String nodeName, String id) { + final String shortName = getShortName(nodeName); + node.setShortName(shortName); + + GradientWidget box = new GradientWidget(this, node); + box.setLayout(LayoutFactory.createVerticalFlowLayout()); + box.setBorder(BorderFactory.createLineBorder(2, Color.BLACK)); + box.setOpaque(true); + + LabelWidget label = new LabelWidget(this); + label.setFont(getDefaultFont().deriveFont(Font.PLAIN, 12.0f)); + label.setLabel(shortName); + label.setBorder(BorderFactory.createEmptyBorder(6, 6, 0, 6)); + label.setAlignment(LabelWidget.Alignment.CENTER); + + box.addChild(label); + + label = new LabelWidget(this); + label.setFont(getDefaultFont().deriveFont(Font.PLAIN, 10.0f)); + label.setLabel(getAddress(nodeName)); + label.setBorder(BorderFactory.createEmptyBorder(3, 6, 0, 6)); + label.setAlignment(LabelWidget.Alignment.CENTER); + + box.addressWidget = label; + + box.addChild(label); + + label = new LabelWidget(this); + label.setFont(getDefaultFont().deriveFont(Font.PLAIN, 10.0f)); + label.setLabel(id); + label.setBorder(BorderFactory.createEmptyBorder(3, 6, 6, 6)); + label.setAlignment(LabelWidget.Alignment.CENTER); + + box.addChild(label); + + return box; + } + + private static String getAddress(String name) { + String[] nameAndHashcode = name.split("@"); + return "@" + nameAndHashcode[1]; + } + + private static String getShortName(String name) { + String[] nameAndHashcode = name.split("@"); + String[] packages = nameAndHashcode[0].split("\\."); + return packages[packages.length - 1]; + } + + @Override + protected Widget attachEdgeWidget(String edge) { + ConnectionWidget connectionWidget = new ConnectionWidget(this); + connectionLayer.addChild(connectionWidget); + return connectionWidget; + } + + @Override + protected void attachEdgeSourceAnchor(String edge, ViewNode oldSourceNode, ViewNode sourceNode) { + final ConnectionWidget connection = (ConnectionWidget) findWidget(edge); + final Widget source = findWidget(sourceNode); + connection.bringToBack(); + source.bringToFront(); + connection.setSourceAnchor(AnchorFactory.createRectangularAnchor(source)); + } + + @Override + protected void attachEdgeTargetAnchor(String edge, ViewNode oldTargetNode, ViewNode targetNode) { + final ConnectionWidget connection = (ConnectionWidget) findWidget(edge); + final Widget target = findWidget(targetNode); + connection.bringToBack(); + target.bringToFront(); + connection.setTargetAnchor(AnchorFactory.createRectangularAnchor(target)); + } + + private static class GradientWidget extends Widget implements ViewNode.StateListener { + public static final GradientPaint BLUE_EXPERIENCE = new GradientPaint( + new Point2D.Double(0, 0), + new Color(168, 204, 241), + new Point2D.Double(0, 1), + new Color(44, 61, 146)); + public static final GradientPaint MAC_OSX_SELECTED = new GradientPaint( + new Point2D.Double(0, 0), + new Color(81, 141, 236), + new Point2D.Double(0, 1), + new Color(36, 96, 192)); + public static final GradientPaint MAC_OSX = new GradientPaint( + new Point2D.Double(0, 0), + new Color(167, 210, 250), + new Point2D.Double(0, 1), + new Color(99, 147, 206)); + public static final GradientPaint AERITH = new GradientPaint( + new Point2D.Double(0, 0), + Color.WHITE, + new Point2D.Double(0, 1), + new Color(64, 110, 161)); + public static final GradientPaint GRAY = new GradientPaint( + new Point2D.Double(0, 0), + new Color(226, 226, 226), + new Point2D.Double(0, 1), + new Color(250, 248, 248)); + public static final GradientPaint RED_XP = new GradientPaint( + new Point2D.Double(0, 0), + new Color(236, 81, 81), + new Point2D.Double(0, 1), + new Color(192, 36, 36)); + public static final GradientPaint NIGHT_GRAY = new GradientPaint( + new Point2D.Double(0, 0), + new Color(102, 111, 127), + new Point2D.Double(0, 1), + new Color(38, 45, 61)); + public static final GradientPaint NIGHT_GRAY_LIGHT = new GradientPaint( + new Point2D.Double(0, 0), + new Color(129, 138, 155), + new Point2D.Double(0, 1), + new Color(58, 66, 82)); + public static final GradientPaint NIGHT_GRAY_VERY_LIGHT = new GradientPaint( + new Point2D.Double(0, 0), + new Color(129, 138, 155, 60), + new Point2D.Double(0, 1), + new Color(58, 66, 82, 60)); + + private static Color UNSELECTED = Color.BLACK; + private static Color SELECTED = Color.WHITE; + + private final ViewNode node; + + private LabelWidget addressWidget; + + private boolean isSelected = false; + private final GradientPaint selectedGradient = MAC_OSX_SELECTED; + private final GradientPaint filteredGradient = RED_XP; + private final GradientPaint focusGradient = NIGHT_GRAY_VERY_LIGHT; + + public GradientWidget(ViewHierarchyScene scene, ViewNode node) { + super(scene); + this.node = node; + node.setStateListener(this); + } + + @Override + protected void notifyStateChanged(ObjectState previous, ObjectState state) { + super.notifyStateChanged(previous, state); + isSelected = state.isSelected() || state.isFocused() || state.isWidgetFocused(); + + pickChildrenColor(); + } + + private void pickChildrenColor() { + for (Widget child : getChildren()) { + child.setForeground(isSelected || node.filtered ? SELECTED : UNSELECTED); + } + + repaint(); + } + + @Override + protected void paintBackground() { + super.paintBackground(); + + Graphics2D g2 = getGraphics(); + Rectangle bounds = getBounds(); + + if (!isSelected) { + if (!node.filtered) { + if (!node.hasFocus) { + g2.setColor(Color.WHITE); + } else { + g2.setPaint(new GradientPaint(bounds.x, bounds.y, + focusGradient.getColor1(), bounds.x, bounds.x + bounds.height, + focusGradient.getColor2())); + } + } else { + g2.setPaint(new GradientPaint(bounds.x, bounds.y, filteredGradient.getColor1(), + bounds.x, bounds.x + bounds.height, filteredGradient.getColor2())); + } + } else { + g2.setPaint(new GradientPaint(bounds.x, bounds.y, selectedGradient.getColor1(), + bounds.x, bounds.x + bounds.height, selectedGradient.getColor2())); + } + g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height); + } + + public void nodeStateChanged(ViewNode node) { + pickChildrenColor(); + } + + public void nodeIndexChanged(ViewNode node) { + if (addressWidget != null) { + addressWidget.setLabel("#" + node.index + addressWidget.getLabel()); + } + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java new file mode 100644 index 000000000..ba346dfa1 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.scene; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.Window; +import com.android.hierarchyviewer.device.DeviceBridge; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; + +public class ViewManager { + public static void invalidate(IDevice device, Window window, String params) { + sendCommand("INVALIDATE", device, window, params); + } + + public static void requestLayout(IDevice device, Window window, String params) { + sendCommand("REQUEST_LAYOUT", device, window, params); + } + + public static void outputDisplayList(IDevice device, Window window, String params) { + sendCommand("OUTPUT_DISPLAYLIST", device, window, params); + } + + private static void sendCommand(String command, IDevice device, Window window, String params) { + Socket socket = null; + BufferedWriter out = null; + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + + out.write(command + " " + window.encode() + " " + params); + out.newLine(); + out.flush(); + } catch (IOException e) { + // Empty + } finally { + try { + if (out != null) { + out.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java new file mode 100644 index 000000000..64c070353 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.scene; + +import java.awt.Image; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class ViewNode { + public String id; + public String name; + + public List<Property> properties = new ArrayList<Property>(); + public Map<String, Property> namedProperties = new HashMap<String, Property>(); + + public ViewNode parent; + public List<ViewNode> children = new ArrayList<ViewNode>(); + + public Image image; + + public int left; + public int top; + public int width; + public int height; + public int scrollX; + public int scrollY; + public int paddingLeft; + public int paddingRight; + public int paddingTop; + public int paddingBottom; + public int marginLeft; + public int marginRight; + public int marginTop; + public int marginBottom; + public int baseline; + public boolean willNotDraw; + public boolean hasMargins; + + boolean hasFocus; + int index; + + public boolean decoded; + public boolean filtered; + + private String shortName; + private StateListener listener; + + void decode() { + id = namedProperties.get("mID").value; + + left = getInt("mLeft", 0); + top = getInt("mTop", 0); + width = getInt("getWidth()", 0); + height = getInt("getHeight()", 0); + scrollX = getInt("mScrollX", 0); + scrollY = getInt("mScrollY", 0); + paddingLeft = getInt("mPaddingLeft", 0); + paddingRight = getInt("mPaddingRight", 0); + paddingTop = getInt("mPaddingTop", 0); + paddingBottom = getInt("mPaddingBottom", 0); + marginLeft = getInt("layout_leftMargin", Integer.MIN_VALUE); + marginRight = getInt("layout_rightMargin", Integer.MIN_VALUE); + marginTop = getInt("layout_topMargin", Integer.MIN_VALUE); + marginBottom = getInt("layout_bottomMargin", Integer.MIN_VALUE); + baseline = getInt("getBaseline()", 0); + willNotDraw = getBoolean("willNotDraw()", false); + hasFocus = getBoolean("hasFocus()", false); + + hasMargins = marginLeft != Integer.MIN_VALUE && + marginRight != Integer.MIN_VALUE && + marginTop != Integer.MIN_VALUE && + marginBottom != Integer.MIN_VALUE; + + decoded = true; + } + + private boolean getBoolean(String name, boolean defaultValue) { + Property p = namedProperties.get(name); + if (p != null) { + try { + return Boolean.parseBoolean(p.value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + private int getInt(String name, int defaultValue) { + Property p = namedProperties.get(name); + if (p != null) { + try { + return Integer.parseInt(p.value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + public void filter(Pattern pattern) { + if (pattern == null || pattern.pattern().length() == 0) { + filtered = false; + } else { + filtered = pattern.matcher(shortName).find() || pattern.matcher(id).find(); + } + listener.nodeStateChanged(this); + } + + void computeIndex() { + index = parent == null ? 0 : parent.children.indexOf(this); + listener.nodeIndexChanged(this); + } + + void setShortName(String shortName) { + this.shortName = shortName; + } + + void setStateListener(StateListener listener) { + this.listener = listener; + } + + @SuppressWarnings({"StringEquality"}) + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ViewNode other = (ViewNode) obj; + return !(this.name != other.name && (this.name == null || !this.name.equals(other.name))); + } + + @Override + public String toString() { + return name; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 67 * hash + (this.name != null ? this.name.hashCode() : 0); + return hash; + } + + public static class Property { + public String name; + public String value; + + @Override + public String toString() { + return name + '=' + value; + } + + @SuppressWarnings({"StringEquality"}) + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Property other = (Property) obj; + if (this.name != other.name && (this.name == null || !this.name.equals(other.name))) { + return false; + } + return !(this.value != other.value && (this.value == null || !this.value.equals(other.value))); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 61 * hash + (this.name != null ? this.name.hashCode() : 0); + hash = 61 * hash + (this.value != null ? this.value.hashCode() : 0); + return hash; + } + } + + interface StateListener { + void nodeStateChanged(ViewNode node); + void nodeIndexChanged(ViewNode node); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/scene/WindowsLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/WindowsLoader.java new file mode 100644 index 000000000..75f179f88 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/WindowsLoader.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.scene; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.DeviceBridge; +import com.android.hierarchyviewer.device.Window; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; + +public class WindowsLoader { + public static Window[] loadWindows(IDevice device, int protocol, int server) { + Socket socket = null; + BufferedReader in = null; + BufferedWriter out = null; + System.out.println("protocol = " + protocol); + System.out.println("version = " + server); + try { + ArrayList<Window> windows = new ArrayList<Window>(); + + socket = new Socket(); + socket.connect(new InetSocketAddress("127.0.0.1", + DeviceBridge.getDeviceLocalPort(device))); + + out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + + out.write("LIST"); + out.newLine(); + out.flush(); + + String line; + while ((line = in.readLine()) != null) { + if ("DONE.".equalsIgnoreCase(line)) { + break; + } + + int index = line.indexOf(' '); + if (index != -1) { + String windowId = line.substring(0, index); + + int id; + if (server > 2) { + id = (int) Long.parseLong(windowId, 16); + } else { + id = Integer.parseInt(windowId, 16); + } + + Window w = new Window(line.substring(index + 1), id); + windows.add(w); + } + } + + return windows.toArray(new Window[windows.size()]); + } catch (IOException e) { + // Empty + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + if (socket != null) { + socket.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + return new Window[0]; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/CaptureRenderer.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/CaptureRenderer.java new file mode 100644 index 000000000..7ccc81835 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/CaptureRenderer.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui; + +import com.android.hierarchyviewer.scene.ViewNode; + +import javax.swing.*; +import java.awt.*; + +class CaptureRenderer extends JLabel { + private ViewNode node; + private boolean showExtras; + + CaptureRenderer(ImageIcon icon, ViewNode node) { + super(icon); + this.node = node; + setBackground(Color.BLACK); + } + + @Override + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + + if (node.hasMargins) { + d.width += node.marginLeft + node.marginRight; + d.height += node.marginTop + node.marginBottom; + } + + return d; + } + + public void setShowExtras(boolean showExtras) { + this.showExtras = showExtras; + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + Icon icon = getIcon(); + int width = icon.getIconWidth(); + int height = icon.getIconHeight(); + + int x = (getWidth() - width) / 2; + int y = (getHeight() - height) / 2; + + icon.paintIcon(this, g, x, y); + + if (showExtras) { + g.translate(x, y); + g.setXORMode(Color.WHITE); + if ((node.paddingBottom | node.paddingLeft | + node.paddingTop | node.paddingRight) != 0) { + g.setColor(Color.RED); + g.drawRect(node.paddingLeft, node.paddingTop, + width - node.paddingRight - node.paddingLeft, + height - node.paddingBottom - node.paddingTop); + } + if (node.baseline != -1) { + g.setColor(Color.BLUE); + g.drawLine(0, node.baseline, width, node.baseline); + } + if (node.hasMargins && (node.marginLeft | node.marginBottom | + node.marginRight | node.marginRight) != 0) { + g.setColor(Color.BLACK); + g.drawRect(-node.marginLeft, -node.marginTop, + node.marginLeft + width + node.marginRight, + node.marginTop + height + node.marginBottom); + } + g.translate(-x, -y); + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java new file mode 100644 index 000000000..89438340d --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui; + +import com.android.hierarchyviewer.scene.ViewHierarchyScene; +import com.android.hierarchyviewer.scene.ViewNode; + +import javax.swing.JComponent; +import javax.swing.BorderFactory; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Set; + +class LayoutRenderer extends JComponent { + private static final int EMULATED_SCREEN_WIDTH = 320; + private static final int EMULATED_SCREEN_HEIGHT = 480; + private static final int SCREEN_MARGIN = 24; + + private boolean showExtras; + private ViewHierarchyScene scene; + private JComponent sceneView; + + LayoutRenderer(ViewHierarchyScene scene, JComponent sceneView) { + this.scene = scene; + this.sceneView = sceneView; + + setOpaque(true); + setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0)); + setBackground(Color.BLACK); + setForeground(Color.WHITE); + + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + selectChild(event.getX(), event.getY()); + } + }); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(EMULATED_SCREEN_WIDTH + SCREEN_MARGIN, + EMULATED_SCREEN_HEIGHT + SCREEN_MARGIN); + } + + @Override + protected void paintComponent(Graphics g) { + g.setColor(getBackground()); + g.fillRect(0, 0, getWidth(), getHeight()); + + Insets insets = getInsets(); + g.clipRect(insets.left, insets.top, + getWidth() - insets.left - insets.right, + getHeight() - insets.top - insets.bottom); + + if (scene == null) { + return; + } + + ViewNode root = scene.getRoot(); + if (root == null) { + return; + } + + int x = (getWidth() - insets.left - insets.right - root.width) / 2; + int y = (getHeight() - insets.top - insets.bottom - root.height) / 2; + g.translate(insets.left + x, insets.top + y); + + g.setColor(getForeground()); + g.drawRect(root.left, root.top, root.width - 1, root.height - 1); + g.clipRect(root.left - 1, root.top - 1, root.width + 1, root.height + 1); + drawChildren(g, root, -root.scrollX, -root.scrollY); + + Set<?> selection = scene.getSelectedObjects(); + if (selection.size() > 0) { + ViewNode node = (ViewNode) selection.iterator().next(); + g.setColor(Color.RED); + Graphics s = g.create(); + ViewNode p = node.parent; + while (p != null) { + s.translate(p.left - p.scrollX, p.top - p.scrollY); + p = p.parent; + } + if (showExtras && node.image != null) { + s.drawImage(node.image, node.left, node.top, null); + } + s.drawRect(node.left, node.top, node.width - 1, node.height - 1); + s.dispose(); + } + + g.translate(-insets.left - x, -insets.top - y); + } + + private void drawChildren(Graphics g, ViewNode root, int x, int y) { + g.translate(x, y); + for (ViewNode node : root.children) { + if (!node.willNotDraw) { + g.drawRect(node.left, node.top, node.width - 1, node.height - 1); + } + + if (node.children.size() > 0) { + drawChildren(g, node, + node.left - node.parent.scrollX, + node.top - node.parent.scrollY); + } + } + g.translate(-x, -y); + } + + public void setShowExtras(boolean showExtras) { + this.showExtras = showExtras; + repaint(); + } + + private void selectChild(int x, int y) { + + if (scene == null) { + return; + } + + ViewNode root = scene.getRoot(); + if (root == null) { + return; + } + + Insets insets = getInsets(); + + int xoffset = (getWidth() - insets.left - insets.right - root.width) / 2 + insets.left + 1; + int yoffset = (getHeight() - insets.top - insets.bottom - root.height) / 2 + insets.top + 1; + + x -= xoffset; + y -= yoffset; + if (x >= 0 && x < EMULATED_SCREEN_WIDTH && y >= 0 && y < EMULATED_SCREEN_HEIGHT) { + ViewNode hit = findChild(root, root, x, y); + scene.setFocusedObject(hit); + sceneView.repaint(); + } + } + + private ViewNode findChild(ViewNode root, ViewNode besthit, int x, int y) { + ViewNode hit = besthit; + for (ViewNode node : root.children) { + + if (node.left <= x && x < node.left + node.width && + node.top <= y && y < node.top + node.height) { + if (node.width <= hit.width && node.height <= hit.height) { + hit = node; + } + } + + if (node.children.size() > 0) { + hit = findChild(node, hit, + x - (node.left - node.parent.scrollX), + y - (node.top - node.parent.scrollY)); + } + } + return hit; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java new file mode 100644 index 000000000..bae1270cc --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java @@ -0,0 +1,831 @@ +package com.android.hierarchyviewer.ui; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.RawImage; +import com.android.hierarchyviewer.scene.ViewNode; +import com.android.hierarchyviewer.ui.util.IconLoader; +import com.android.hierarchyviewer.ui.util.PngFileFilter; +import com.android.hierarchyviewer.util.WorkerThread; + +import java.awt.AlphaComposite; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.Timer; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +class ScreenViewer extends JPanel implements ActionListener { + private final Workspace workspace; + private final IDevice device; + + private GetScreenshotTask task; + private BufferedImage image; + private int[] scanline; + private volatile boolean isLoading; + + private BufferedImage overlay; + private AlphaComposite overlayAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f); + + private ScreenViewer.LoupeStatus status; + private ScreenViewer.LoupeViewer loupe; + private ScreenViewer.Crosshair crosshair; + + private int zoom = 8; + private int y = 0; + + private Timer timer; + private ViewNode node; + + private JSlider zoomSlider; + + ScreenViewer(Workspace workspace, IDevice device, int spacing) { + setLayout(new GridBagLayout()); + setOpaque(false); + + this.workspace = workspace; + this.device = device; + + timer = new Timer(5000, this); + timer.setInitialDelay(0); + timer.setRepeats(true); + + JPanel panel = buildViewerAndControls(); + add(panel, new GridBagConstraints(0, 0, 1, 1, 0.3f, 1.0f, + GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); + + JPanel loupePanel = buildLoupePanel(spacing); + add(loupePanel, new GridBagConstraints(1, 0, 1, 1, 0.7f, 1.0f, + GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + timer.start(); + } + }); + } + + private JPanel buildLoupePanel(int spacing) { + loupe = new LoupeViewer(); + loupe.addMouseWheelListener(new WheelZoomListener()); + CrosshairPanel crosshairPanel = new CrosshairPanel(loupe); + + JPanel loupePanel = new JPanel(new BorderLayout()); + loupePanel.add(crosshairPanel); + status = new LoupeStatus(); + loupePanel.add(status, BorderLayout.SOUTH); + + loupePanel.setBorder(BorderFactory.createEmptyBorder(0, spacing, 0, 0)); + return loupePanel; + } + + private class WheelZoomListener implements MouseWheelListener { + public void mouseWheelMoved(MouseWheelEvent e) { + if (zoomSlider != null) { + int val = zoomSlider.getValue(); + val -= e.getWheelRotation() * 2; + zoomSlider.setValue(val); + } + } + } + + private JPanel buildViewerAndControls() { + JPanel panel = new JPanel(new GridBagLayout()); + crosshair = new Crosshair(new ScreenshotViewer()); + crosshair.addMouseWheelListener(new WheelZoomListener()); + JScrollPane scroller = new JScrollPane(crosshair); + scroller.setPreferredSize(new Dimension(320, 480)); + scroller.setBorder(null); + panel.add(scroller, + new GridBagConstraints(0, y++, 2, 1, 1.0f, 1.0f, + GridBagConstraints.FIRST_LINE_START, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0)); + buildSlider(panel, "Overlay:", "0%", "100%", 0, 100, 30, 1).addChangeListener( + new ChangeListener() { + public void stateChanged(ChangeEvent event) { + float opacity = ((JSlider) event.getSource()).getValue() / 100.0f; + overlayAlpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity); + repaint(); + } + }); + buildOverlayExtraControls(panel); + buildSlider(panel, "Refresh Rate:", "1s", "40s", 1, 40, 5, 1).addChangeListener( + new ChangeListener() { + public void stateChanged(ChangeEvent event) { + int rate = ((JSlider) event.getSource()).getValue() * 1000; + timer.setDelay(rate); + timer.setInitialDelay(0); + timer.restart(); + } + }); + zoomSlider = buildSlider(panel, "Zoom:", "2x", "24x", 2, 24, 8, 2); + zoomSlider.addChangeListener( + new ChangeListener() { + public void stateChanged(ChangeEvent event) { + zoom = ((JSlider) event.getSource()).getValue(); + loupe.clearGrid = true; + loupe.moveToPoint(crosshair.crosshair.x, crosshair.crosshair.y); + repaint(); + } + }); + panel.add(Box.createVerticalGlue(), + new GridBagConstraints(0, y++, 2, 1, 1.0f, 1.0f, + GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + return panel; + } + + private void buildOverlayExtraControls(JPanel panel) { + JPanel extras = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + + JButton loadOverlay = new JButton("Load..."); + loadOverlay.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + SwingWorker<?, ?> worker = openOverlay(); + if (worker != null) { + worker.execute(); + } + } + }); + extras.add(loadOverlay); + + JCheckBox showInLoupe = new JCheckBox("Show in Loupe"); + showInLoupe.setSelected(false); + showInLoupe.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + loupe.showOverlay = ((JCheckBox) event.getSource()).isSelected(); + loupe.repaint(); + } + }); + extras.add(showInLoupe); + + panel.add(extras, new GridBagConstraints(1, y++, 1, 1, 1.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + } + + public SwingWorker<?, ?> openOverlay() { + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(new PngFileFilter()); + int choice = chooser.showOpenDialog(this); + if (choice == JFileChooser.APPROVE_OPTION) { + return new OpenOverlayTask(chooser.getSelectedFile()); + } else { + return null; + } + } + + private JSlider buildSlider(JPanel panel, String title, String minName, String maxName, + int min, int max, int value, int tick) { + panel.add(new JLabel(title), new GridBagConstraints(0, y, 1, 1, 1.0f, 0.0f, + GridBagConstraints.LINE_END, GridBagConstraints.NONE, + new Insets(0, 0, 0, 6), 0, 0)); + JPanel sliderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + sliderPanel.add(new JLabel(minName)); + JSlider slider = new JSlider(min, max, value); + slider.setMinorTickSpacing(tick); + slider.setMajorTickSpacing(tick); + slider.setSnapToTicks(true); + sliderPanel.add(slider); + sliderPanel.add(new JLabel(maxName)); + panel.add(sliderPanel, new GridBagConstraints(1, y++, 1, 1, 1.0f, 0.0f, + GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 0), 0, 0)); + return slider; + } + + void stop() { + timer.stop(); + } + + void start() { + timer.start(); + } + + void select(ViewNode node) { + this.node = node; + repaint(); + } + + class LoupeViewer extends JComponent { + private final Color lineColor = new Color(1.0f, 1.0f, 1.0f, 0.3f); + + private int width; + private int height; + private BufferedImage grid; + private int left; + private int top; + public boolean clearGrid; + + private final Rectangle clip = new Rectangle(); + private boolean showOverlay = false; + + LoupeViewer() { + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + moveToPoint(event); + } + }); + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent event) { + moveToPoint(event); + } + }); + } + + @Override + protected void paintComponent(Graphics g) { + if (isLoading) { + return; + } + + g.translate(-left, -top); + + if (image != null) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g2.scale(zoom, zoom); + g2.drawImage(image, 0, 0, null); + if (overlay != null && showOverlay) { + g2.setComposite(overlayAlpha); + g2.drawImage(overlay, 0, image.getHeight() - overlay.getHeight(), null); + } + g2.dispose(); + } + + int width = getWidth(); + int height = getHeight(); + + Graphics2D g2 = null; + if (width != this.width || height != this.height) { + this.width = width; + this.height = height; + + grid = new BufferedImage(width + zoom + 1, height + zoom + 1, + BufferedImage.TYPE_INT_ARGB); + clearGrid = true; + g2 = grid.createGraphics(); + } else if (clearGrid) { + g2 = grid.createGraphics(); + g2.setComposite(AlphaComposite.Clear); + g2.fillRect(0, 0, grid.getWidth(), grid.getHeight()); + g2.setComposite(AlphaComposite.SrcOver); + } + + if (clearGrid) { + clearGrid = false; + + g2.setColor(lineColor); + width += zoom; + height += zoom; + + for (int x = zoom; x <= width; x += zoom) { + g2.drawLine(x, 0, x, height); + } + + for (int y = 0; y <= height; y += zoom) { + g2.drawLine(0, y, width, y); + } + + g2.dispose(); + } + + if (image != null) { + g.getClipBounds(clip); + g.clipRect(0, 0, image.getWidth() * zoom + 1, image.getHeight() * zoom + 1); + g.drawImage(grid, clip.x - clip.x % zoom, clip.y - clip.y % zoom, null); + } + + g.translate(left, top); + } + + void moveToPoint(MouseEvent event) { + int x = Math.max(0, Math.min((event.getX() + left) / zoom, image.getWidth() - 1)); + int y = Math.max(0, Math.min((event.getY() + top) / zoom, image.getHeight() - 1)); + moveToPoint(x, y); + crosshair.moveToPoint(x, y); + } + + void moveToPoint(int x, int y) { + left = x * zoom - width / 2 + zoom / 2; + top = y * zoom - height / 2 + zoom / 2; + repaint(); + } + } + + class LoupeStatus extends JPanel { + private JLabel xLabel; + private JLabel yLabel; + private JLabel rLabel; + private JLabel gLabel; + private JLabel bLabel; + private JLabel hLabel; + private ScreenViewer.LoupeStatus.ColoredSquare square; + private Color color; + + LoupeStatus() { + setOpaque(true); + setLayout(new GridBagLayout()); + setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); + + square = new ColoredSquare(); + add(square, new GridBagConstraints(0, 0, 1, 2, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + + JLabel label; + + add(label = new JLabel("#ffffff"), new GridBagConstraints(0, 2, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + label.setForeground(Color.WHITE); + hLabel = label; + + add(label = new JLabel("R:"), new GridBagConstraints(1, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 6, 0, 6), 0, 0 )); + label.setForeground(Color.WHITE); + add(label = new JLabel("255"), new GridBagConstraints(2, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + label.setForeground(Color.WHITE); + rLabel = label; + + add(label = new JLabel("G:"), new GridBagConstraints(1, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 6, 0, 6), 0, 0 )); + label.setForeground(Color.WHITE); + add(label = new JLabel("255"), new GridBagConstraints(2, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + label.setForeground(Color.WHITE); + gLabel = label; + + add(label = new JLabel("B:"), new GridBagConstraints(1, 2, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 6, 0, 6), 0, 0 )); + label.setForeground(Color.WHITE); + add(label = new JLabel("255"), new GridBagConstraints(2, 2, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + label.setForeground(Color.WHITE); + bLabel = label; + + add(label = new JLabel("X:"), new GridBagConstraints(3, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 6, 0, 6), 0, 0 )); + label.setForeground(Color.WHITE); + add(label = new JLabel("0 px"), new GridBagConstraints(4, 0, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + label.setForeground(Color.WHITE); + xLabel = label; + + add(label = new JLabel("Y:"), new GridBagConstraints(3, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 6, 0, 6), 0, 0 )); + label.setForeground(Color.WHITE); + add(label = new JLabel("0 px"), new GridBagConstraints(4, 1, 1, 1, 0.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.NONE, + new Insets(0, 0, 0, 12), 0, 0 )); + label.setForeground(Color.WHITE); + yLabel = label; + + add(Box.createHorizontalGlue(), new GridBagConstraints(5, 0, 1, 1, 1.0f, 0.0f, + GridBagConstraints.LINE_START, GridBagConstraints.BOTH, + new Insets(0, 0, 0, 0), 0, 0 )); + } + + @Override + protected void paintComponent(Graphics g) { + g.setColor(Color.BLACK); + g.fillRect(0, 0, getWidth(), getHeight()); + } + + void showPixel(int x, int y) { + xLabel.setText(x + " px"); + yLabel.setText(y + " px"); + + int pixel = image.getRGB(x, y); + color = new Color(pixel); + hLabel.setText("#" + Integer.toHexString(pixel)); + rLabel.setText(String.valueOf((pixel >> 16) & 0xff)); + gLabel.setText(String.valueOf((pixel >> 8) & 0xff)); + bLabel.setText(String.valueOf((pixel ) & 0xff)); + + square.repaint(); + } + + private class ColoredSquare extends JComponent { + @Override + public Dimension getPreferredSize() { + Dimension d = super.getPreferredSize(); + d.width = 60; + d.height = 30; + return d; + } + + @Override + protected void paintComponent(Graphics g) { + g.setColor(color); + g.fillRect(0, 0, getWidth(), getHeight()); + + g.setColor(Color.WHITE); + g.drawRect(0, 0, getWidth() - 1, getHeight() - 1); + } + } + } + + class Crosshair extends JPanel { + // magenta = 0xff5efe + private final Color crosshairColor = new Color(0x00ffff); + Point crosshair = new Point(); + private int width; + private int height; + private final ScreenshotViewer screenshotViewer; + + Crosshair(ScreenshotViewer screenshotViewer) { + this.screenshotViewer = screenshotViewer; + setOpaque(true); + setLayout(new BorderLayout()); + add(screenshotViewer); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent event) { + moveToPoint(event); + } + }); + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent event) { + moveToPoint(event); + } + }); + } + + void moveToPoint(int x, int y) { + crosshair.x = x; + crosshair.y = y; + status.showPixel(crosshair.x, crosshair.y); + repaint(); + } + + private void moveToPoint(MouseEvent event) { + crosshair.x = Math.max(0, Math.min(image.getWidth() - 1, event.getX())); + crosshair.y = Math.max(0, Math.min(image.getHeight() - 1, event.getY())); + loupe.moveToPoint(crosshair.x, crosshair.y); + status.showPixel(crosshair.x, crosshair.y); + + repaint(); + } + + @Override + public Dimension getPreferredSize() { + return screenshotViewer.getPreferredSize(); + } + + @Override + public Dimension getMaximumSize() { + return screenshotViewer.getPreferredSize(); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + + if (crosshair == null || width != getWidth() || height != getHeight()) { + width = getWidth(); + height = getHeight(); + crosshair = new Point(width / 2, height / 2); + } + + g.setColor(crosshairColor); + + g.drawLine(crosshair.x, 0, crosshair.x, height); + g.drawLine(0, crosshair.y, width, crosshair.y); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + g.setColor(Color.BLACK); + g.fillRect(0, 0, getWidth(), getHeight()); + } + } + + class ScreenshotViewer extends JComponent { + private final Color boundsColor = new Color(0xff5efe); + + ScreenshotViewer() { + setOpaque(true); + } + + @Override + protected void paintComponent(Graphics g) { + g.setColor(Color.BLACK); + g.fillRect(0, 0, getWidth(), getHeight()); + + if (isLoading) { + return; + } + + if (image != null) { + g.drawImage(image, 0, 0, null); + if (overlay != null) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setComposite(overlayAlpha); + g2.drawImage(overlay, 0, image.getHeight() - overlay.getHeight(), null); + } + } + + if (node != null) { + Graphics s = g.create(); + s.setColor(boundsColor); + ViewNode p = node.parent; + while (p != null) { + s.translate(p.left - p.scrollX, p.top - p.scrollY); + p = p.parent; + } + s.drawRect(node.left, node.top, node.width - 1, node.height - 1); + s.translate(node.left, node.top); + + s.setXORMode(Color.WHITE); + if ((node.paddingBottom | node.paddingLeft | + node.paddingTop | node.paddingRight) != 0) { + s.setColor(Color.BLACK); + s.drawRect(node.paddingLeft, node.paddingTop, + node.width - node.paddingRight - node.paddingLeft - 1, + node.height - node.paddingBottom - node.paddingTop - 1); + } + if (node.hasMargins && (node.marginLeft | node.marginBottom | + node.marginRight | node.marginRight) != 0) { + s.setColor(Color.BLACK); + s.drawRect(-node.marginLeft, -node.marginTop, + node.marginLeft + node.width + node.marginRight - 1, + node.marginTop + node.height + node.marginBottom - 1); + } + + s.dispose(); + } + } + + @Override + public Dimension getPreferredSize() { + if (image == null) { + return new Dimension(320, 480); + } + return new Dimension(image.getWidth(), image.getHeight()); + } + } + + private class CrosshairPanel extends JPanel { + private final Color crosshairColor = new Color(0xff5efe); + private final Insets insets = new Insets(0, 0, 0, 0); + + CrosshairPanel(LoupeViewer loupe) { + setLayout(new BorderLayout()); + add(loupe); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + + g.setColor(crosshairColor); + + int width = getWidth(); + int height = getHeight(); + + getInsets(insets); + + int x = (width - insets.left - insets.right) / 2; + int y = (height - insets.top - insets.bottom) / 2; + + g.drawLine(insets.left + x, insets.top, insets.left + x, height - insets.bottom); + g.drawLine(insets.left, insets.top + y, width - insets.right, insets.top + y); + } + + @Override + protected void paintComponent(Graphics g) { + g.setColor(Color.BLACK); + Insets insets = getInsets(); + g.fillRect(insets.left, insets.top, getWidth() - insets.left - insets.right, + getHeight() - insets.top - insets.bottom); + } + } + + public void actionPerformed(ActionEvent event) { + if (task != null && !task.isDone()) { + return; + } + task = new GetScreenshotTask(); + task.execute(); + } + + private class GetScreenshotTask extends SwingWorker<Boolean, Void> { + private GetScreenshotTask() { + workspace.beginTask(); + } + + @Override + @WorkerThread + protected Boolean doInBackground() throws Exception { + RawImage rawImage; + try { + rawImage = device.getScreenshot(); + } catch (IOException ioe) { + return false; + } + + boolean resize = false; + isLoading = true; + try { + if (rawImage != null) { + if (image == null || rawImage.width != image.getWidth() || + rawImage.height != image.getHeight()) { + image = new BufferedImage(rawImage.width, rawImage.height, + BufferedImage.TYPE_INT_ARGB); + scanline = new int[rawImage.width]; + resize = true; + } + + switch (rawImage.bpp) { + case 16: + rawImage16toARGB(rawImage); + break; + case 32: + rawImage32toARGB(rawImage); + break; + } + } + } finally { + isLoading = false; + } + + return resize; + } + + private int getMask(int length) { + int res = 0; + for (int i = 0 ; i < length ; i++) { + res = (res << 1) + 1; + } + + return res; + } + + private void rawImage32toARGB(RawImage rawImage) { + byte[] buffer = rawImage.data; + int index = 0; + + final int redOffset = rawImage.red_offset; + final int redLength = rawImage.red_length; + final int redMask = getMask(redLength); + final int greenOffset = rawImage.green_offset; + final int greenLength = rawImage.green_length; + final int greenMask = getMask(greenLength); + final int blueOffset = rawImage.blue_offset; + final int blueLength = rawImage.blue_length; + final int blueMask = getMask(blueLength); + final int alphaLength = rawImage.alpha_length; + final int alphaOffset = rawImage.alpha_offset; + final int alphaMask = getMask(alphaLength); + + for (int y = 0 ; y < rawImage.height ; y++) { + for (int x = 0 ; x < rawImage.width ; x++) { + int value = buffer[index++] & 0x00FF; + value |= (buffer[index++] & 0x00FF) << 8; + value |= (buffer[index++] & 0x00FF) << 16; + value |= (buffer[index++] & 0x00FF) << 24; + + int r = ((value >>> redOffset) & redMask) << (8 - redLength); + int g = ((value >>> greenOffset) & greenMask) << (8 - greenLength); + int b = ((value >>> blueOffset) & blueMask) << (8 - blueLength); + int a = 0xFF; + + if (alphaLength != 0) { + a = ((value >>> alphaOffset) & alphaMask) << (8 - alphaLength); + } + + scanline[x] = a << 24 | r << 16 | g << 8 | b; + } + + image.setRGB(0, y, rawImage.width, 1, scanline, + 0, rawImage.width); + } + } + + private void rawImage16toARGB(RawImage rawImage) { + byte[] buffer = rawImage.data; + int index = 0; + + for (int y = 0 ; y < rawImage.height ; y++) { + for (int x = 0 ; x < rawImage.width ; x++) { + int value = buffer[index++] & 0x00FF; + value |= (buffer[index++] << 8) & 0x0FF00; + + int r = ((value >> 11) & 0x01F) << 3; + int g = ((value >> 5) & 0x03F) << 2; + int b = ((value ) & 0x01F) << 3; + + scanline[x] = 0xFF << 24 | r << 16 | g << 8 | b; + } + + image.setRGB(0, y, rawImage.width, 1, scanline, + 0, rawImage.width); + } + } + + @Override + protected void done() { + workspace.endTask(); + try { + if (get()) { + validate(); + crosshair.crosshair = new Point(image.getWidth() / 2, + image.getHeight() / 2); + status.showPixel(image.getWidth() / 2, image.getHeight() / 2); + loupe.moveToPoint(image.getWidth() / 2, image.getHeight() / 2); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + repaint(); + } + } + + private class OpenOverlayTask extends SwingWorker<BufferedImage, Void> { + private File file; + + private OpenOverlayTask(File file) { + this.file = file; + workspace.beginTask(); + } + + @Override + @WorkerThread + protected BufferedImage doInBackground() { + try { + return IconLoader.toCompatibleImage(ImageIO.read(file)); + } catch (IOException ex) { + ex.printStackTrace(); + } + return null; + } + + @Override + protected void done() { + try { + overlay = get(); + repaint(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } finally { + workspace.endTask(); + } + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java new file mode 100644 index 000000000..bfa15b3d4 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java @@ -0,0 +1,1621 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewer.device.DeviceBridge; +import com.android.hierarchyviewer.device.Window; +import com.android.hierarchyviewer.laf.UnifiedContentBorder; +import com.android.hierarchyviewer.scene.CaptureLoader; +import com.android.hierarchyviewer.scene.ProfilesLoader; +import com.android.hierarchyviewer.scene.VersionLoader; +import com.android.hierarchyviewer.scene.ViewHierarchyLoader; +import com.android.hierarchyviewer.scene.ViewHierarchyScene; +import com.android.hierarchyviewer.scene.ViewManager; +import com.android.hierarchyviewer.scene.ViewNode; +import com.android.hierarchyviewer.scene.WindowsLoader; +import com.android.hierarchyviewer.ui.action.CaptureLayersAction; +import com.android.hierarchyviewer.ui.action.CaptureNodeAction; +import com.android.hierarchyviewer.ui.action.DumpDisplayListAction; +import com.android.hierarchyviewer.ui.action.ExitAction; +import com.android.hierarchyviewer.ui.action.InvalidateAction; +import com.android.hierarchyviewer.ui.action.LoadGraphAction; +import com.android.hierarchyviewer.ui.action.RefreshWindowsAction; +import com.android.hierarchyviewer.ui.action.RequestLayoutAction; +import com.android.hierarchyviewer.ui.action.SaveSceneAction; +import com.android.hierarchyviewer.ui.action.ShowDevicesAction; +import com.android.hierarchyviewer.ui.action.StartServerAction; +import com.android.hierarchyviewer.ui.action.StopServerAction; +import com.android.hierarchyviewer.ui.model.ProfilesTableModel; +import com.android.hierarchyviewer.ui.model.PropertiesTableModel; +import com.android.hierarchyviewer.ui.model.ViewsTreeModel; +import com.android.hierarchyviewer.ui.util.IconLoader; +import com.android.hierarchyviewer.ui.util.PngFileFilter; +import com.android.hierarchyviewer.ui.util.PsdFileFilter; +import com.android.hierarchyviewer.util.OS; +import com.android.hierarchyviewer.util.WorkerThread; + +import org.netbeans.api.visual.graph.layout.TreeGraphLayout; +import org.netbeans.api.visual.model.ObjectSceneEvent; +import org.netbeans.api.visual.model.ObjectSceneEventType; +import org.netbeans.api.visual.model.ObjectSceneListener; +import org.netbeans.api.visual.model.ObjectState; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Image; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.imageio.ImageIO; +import javax.swing.ActionMap; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.ButtonGroup; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JSlider; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.JTree; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.table.DefaultTableModel; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreePath; + +public class Workspace extends JFrame { + private JLabel viewCountLabel; + private JSlider zoomSlider; + private JSplitPane sideSplitter; + private JSplitPane mainSplitter; + private JTable propertiesTable; + private JTable profilingTable; + private JComponent pixelPerfectPanel; + private JTree pixelPerfectTree; + private ScreenViewer screenViewer; + + private JPanel extrasPanel; + private LayoutRenderer layoutView; + + private JScrollPane sceneScroller; + private JComponent sceneView; + + private ViewHierarchyScene scene; + + private ActionMap actionsMap; + private JPanel mainPanel; + private JProgressBar progress; + private JToolBar buttonsPanel; + private JToolBar commandButtonsPanel; + + private JComponent deviceSelector; + private DevicesTableModel devicesTableModel; + private WindowsTableModel windowsTableModel; + + private IDevice currentDevice; + private Window currentWindow = Window.FOCUSED_WINDOW; + + private JButton displayNodeButton; + private JButton dumpDisplayListButton; + private JButton captureLayersButton; + private JButton invalidateButton; + private JButton requestLayoutButton; + private JButton loadButton; + private JButton startButton; + private JButton stopButton; + private JButton showDevicesButton; + private JButton refreshButton; + private JToggleButton graphViewButton; + private JToggleButton pixelPerfectViewButton; + private JMenuItem saveMenuItem; + private JMenuItem showDevicesMenuItem; + private JMenuItem loadMenuItem; + private JMenuItem startMenuItem; + private JMenuItem stopMenuItem; + private JTable devices; + private JTable windows; + private JLabel minZoomLabel; + private JLabel maxZoomLabel; + private JTextField filterText; + private JLabel filterLabel; + + private int protocolVersion; + private int serverVersion; + + public Workspace() { + super("Hierarchy Viewer"); + + buildActions(); + add(buildMainPanel()); + setJMenuBar(buildMenuBar()); + + devices.changeSelection(0, 0, false, false); + currentDeviceChanged(); + + pack(); + } + + private void buildActions() { + actionsMap = new ActionMap(); + actionsMap.put(ExitAction.ACTION_NAME, new ExitAction(this)); + actionsMap.put(ShowDevicesAction.ACTION_NAME, new ShowDevicesAction(this)); + actionsMap.put(LoadGraphAction.ACTION_NAME, new LoadGraphAction(this)); + actionsMap.put(SaveSceneAction.ACTION_NAME, new SaveSceneAction(this)); + actionsMap.put(StartServerAction.ACTION_NAME, new StartServerAction(this)); + actionsMap.put(StopServerAction.ACTION_NAME, new StopServerAction(this)); + actionsMap.put(InvalidateAction.ACTION_NAME, new InvalidateAction(this)); + actionsMap.put(RequestLayoutAction.ACTION_NAME, new RequestLayoutAction(this)); + actionsMap.put(DumpDisplayListAction.ACTION_NAME, new DumpDisplayListAction(this)); + actionsMap.put(CaptureNodeAction.ACTION_NAME, new CaptureNodeAction(this)); + actionsMap.put(CaptureLayersAction.ACTION_NAME, new CaptureLayersAction(this)); + actionsMap.put(RefreshWindowsAction.ACTION_NAME, new RefreshWindowsAction(this)); + } + + private JComponent buildMainPanel() { + mainPanel = new JPanel(); + mainPanel.setLayout(new BorderLayout()); + commandButtonsPanel = buildToolBar(); + mainPanel.add(commandButtonsPanel, BorderLayout.PAGE_START); + mainPanel.add(deviceSelector = buildDeviceSelector(), BorderLayout.CENTER); + mainPanel.add(buildStatusPanel(), BorderLayout.SOUTH); + + mainPanel.setPreferredSize(new Dimension(1200, 800)); + + return mainPanel; + } + + private JComponent buildGraphPanel() { + sceneScroller = new JScrollPane(); + sceneScroller.setBorder(null); + + mainSplitter = new JSplitPane(); + mainSplitter.setResizeWeight(1.0); + mainSplitter.setContinuousLayout(true); + if (OS.isMacOsX() && OS.isLeopardOrLater()) { + mainSplitter.setBorder(new UnifiedContentBorder()); + } + + mainSplitter.setLeftComponent(sceneScroller); + mainSplitter.setRightComponent(buildSideSplitter()); + + return mainSplitter; + } + + private JComponent buildDeviceSelector() { + JPanel panel = new JPanel(new GridBagLayout()); + if (OS.isMacOsX() && OS.isLeopardOrLater()) { + panel.setBorder(new UnifiedContentBorder()); + } + + devicesTableModel = new DevicesTableModel(); + for (IDevice device : DeviceBridge.getDevices()) { + DeviceBridge.setupDeviceForward(device); + devicesTableModel.addDevice(device); + } + DeviceBridge.startListenForDevices(devicesTableModel); + + devices = new JTable(devicesTableModel); + devices.getSelectionModel().addListSelectionListener(new DeviceSelectedListener()); + devices.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + devices.setBorder(null); + JScrollPane devicesScroller = new JScrollPane(devices); + devicesScroller.setBorder(null); + panel.add(devicesScroller, new GridBagConstraints(0, 0, 1, 1, 0.5, 1.0, + GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), + 0, 0)); + + windowsTableModel = new WindowsTableModel(); + windowsTableModel.setVisible(false); + + windows = new JTable(windowsTableModel); + windows.getSelectionModel().addListSelectionListener(new WindowSelectedListener()); + windows.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + windows.setBorder(null); + JScrollPane windowsScroller = new JScrollPane(windows); + windowsScroller.setBorder(null); + panel.add(windowsScroller, new GridBagConstraints(2, 0, 1, 1, 0.5, 1.0, + GridBagConstraints.LINE_START, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), + 0, 0)); + + return panel; + } + + private JComponent buildSideSplitter() { + propertiesTable = new JTable(); + propertiesTable.setModel(new DefaultTableModel(new Object[][] { }, + new String[] { "Property", "Value" })); + propertiesTable.setBorder(null); + propertiesTable.getTableHeader().setBorder(null); + + JScrollPane tableScroller = new JScrollPane(propertiesTable); + tableScroller.setBorder(null); + + profilingTable = new JTable(); + profilingTable.setModel(new DefaultTableModel(new Object[][] { + { " " , " " }, { " " , " " }, { " " , " " } }, + new String[] { "Operation", "Duration (ms)" })); + profilingTable.setBorder(null); + profilingTable.getTableHeader().setBorder(null); + + JScrollPane firstTableScroller = new JScrollPane(profilingTable); + firstTableScroller.setBorder(null); + + setVisibleRowCount(profilingTable, 5); + firstTableScroller.setMinimumSize(profilingTable.getPreferredScrollableViewportSize()); + + JSplitPane tablesSplitter = new JSplitPane(); + tablesSplitter.setBorder(null); + tablesSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT); + tablesSplitter.setResizeWeight(0); + tablesSplitter.setLeftComponent(firstTableScroller); + tablesSplitter.setBottomComponent(tableScroller); + tablesSplitter.setContinuousLayout(true); + + sideSplitter = new JSplitPane(); + sideSplitter.setBorder(null); + sideSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT); + sideSplitter.setResizeWeight(0.5); + sideSplitter.setLeftComponent(tablesSplitter); + sideSplitter.setBottomComponent(null); + sideSplitter.setContinuousLayout(true); + + return sideSplitter; + } + + private JPanel buildStatusPanel() { + JPanel statusPanel = new JPanel(); + statusPanel.setLayout(new BorderLayout()); + + JPanel leftSide = new JPanel(); + leftSide.setOpaque(false); + leftSide.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 5)); + leftSide.add(Box.createHorizontalStrut(6)); + + ButtonGroup group = new ButtonGroup(); + + graphViewButton = new JToggleButton(IconLoader.load(getClass(), + "/images/icon-graph-view.png")); + graphViewButton.setSelectedIcon(IconLoader.load(getClass(), + "/images/icon-graph-view-selected.png")); + graphViewButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + graphViewButton.putClientProperty("JButton.segmentPosition", "first"); + graphViewButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + toggleGraphView(); + } + }); + group.add(graphViewButton); + leftSide.add(graphViewButton); + + pixelPerfectViewButton = new JToggleButton(IconLoader.load(getClass(), + "/images/icon-pixel-perfect-view.png")); + pixelPerfectViewButton.setSelectedIcon(IconLoader.load(getClass(), + "/images/icon-pixel-perfect-view-selected.png")); + pixelPerfectViewButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + pixelPerfectViewButton.putClientProperty("JButton.segmentPosition", "last"); + pixelPerfectViewButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + togglePixelPerfectView(); + } + }); + group.add(pixelPerfectViewButton); + leftSide.add(pixelPerfectViewButton); + + graphViewButton.setSelected(true); + + filterText = new JTextField(20); + filterText.putClientProperty("JComponent.sizeVariant", "small"); + filterText.getDocument().addDocumentListener(new DocumentListener() { + public void insertUpdate(DocumentEvent e) { + updateFilter(e); + } + + public void removeUpdate(DocumentEvent e) { + updateFilter(e); + } + + public void changedUpdate(DocumentEvent e) { + updateFilter(e); + } + }); + + filterLabel = new JLabel("Filter by class or id:"); + filterLabel.putClientProperty("JComponent.sizeVariant", "small"); + filterLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 6)); + + leftSide.add(filterLabel); + leftSide.add(filterText); + + minZoomLabel = new JLabel(); + minZoomLabel.setText("20%"); + minZoomLabel.putClientProperty("JComponent.sizeVariant", "small"); + minZoomLabel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 0)); + leftSide.add(minZoomLabel); + + zoomSlider = new JSlider(); + zoomSlider.putClientProperty("JComponent.sizeVariant", "small"); + zoomSlider.setMaximum(200); + zoomSlider.setMinimum(20); + zoomSlider.setValue(100); + zoomSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent evt) { + zoomSliderStateChanged(evt); + } + }); + leftSide.add(zoomSlider); + + maxZoomLabel = new JLabel(); + maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small"); + maxZoomLabel.setText("200%"); + leftSide.add(maxZoomLabel); + + viewCountLabel = new JLabel(); + viewCountLabel.setText("0 views"); + viewCountLabel.putClientProperty("JComponent.sizeVariant", "small"); + viewCountLabel.setBorder(BorderFactory.createEmptyBorder(0, 12, 0, 0)); + leftSide.add(viewCountLabel); + + statusPanel.add(leftSide, BorderLayout.LINE_START); + + JPanel rightSide = new JPanel(); + rightSide.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 12)); + rightSide.setLayout(new FlowLayout(FlowLayout.RIGHT)); + + progress = new JProgressBar(); + progress.setVisible(false); + progress.setIndeterminate(true); + progress.putClientProperty("JComponent.sizeVariant", "mini"); + progress.putClientProperty("JProgressBar.style", "circular"); + rightSide.add(progress); + + statusPanel.add(rightSide, BorderLayout.LINE_END); + + hideStatusBarComponents(); + + return statusPanel; + } + + private void hideStatusBarComponents() { + viewCountLabel.setVisible(false); + zoomSlider.setVisible(false); + minZoomLabel.setVisible(false); + maxZoomLabel.setVisible(false); + filterLabel.setVisible(false); + filterText.setVisible(false); + } + + private JToolBar buildToolBar() { + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + toolBar.setRollover(true); + + startButton = new JButton(); + startButton.setAction(actionsMap.get(StartServerAction.ACTION_NAME)); + startButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + startButton.putClientProperty("JButton.segmentPosition", "first"); + toolBar.add(startButton); + + stopButton = new JButton(); + stopButton.setAction(actionsMap.get(StopServerAction.ACTION_NAME)); + stopButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + stopButton.putClientProperty("JButton.segmentPosition", "middle"); + toolBar.add(stopButton); + + refreshButton = new JButton(); + refreshButton.setAction(actionsMap.get(RefreshWindowsAction.ACTION_NAME)); + refreshButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + refreshButton.putClientProperty("JButton.segmentPosition", "last"); + toolBar.add(refreshButton); + + showDevicesButton = new JButton(); + showDevicesButton.setAction(actionsMap.get(ShowDevicesAction.ACTION_NAME)); + showDevicesButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + showDevicesButton.putClientProperty("JButton.segmentPosition", "first"); + toolBar.add(showDevicesButton); + showDevicesButton.setEnabled(false); + + loadButton = new JButton(); + loadButton.setAction(actionsMap.get(LoadGraphAction.ACTION_NAME)); + loadButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + loadButton.putClientProperty("JButton.segmentPosition", "last"); + toolBar.add(loadButton); + + displayNodeButton = new JButton(); + displayNodeButton.setAction(actionsMap.get(CaptureNodeAction.ACTION_NAME)); + displayNodeButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + displayNodeButton.putClientProperty("JButton.segmentPosition", "first"); + toolBar.add(displayNodeButton); + + dumpDisplayListButton = new JButton(); + dumpDisplayListButton.setAction(actionsMap.get(DumpDisplayListAction.ACTION_NAME)); + dumpDisplayListButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + dumpDisplayListButton.putClientProperty("JButton.segmentPosition", "middle"); + + captureLayersButton = new JButton(); + captureLayersButton.setAction(actionsMap.get(CaptureLayersAction.ACTION_NAME)); + captureLayersButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + captureLayersButton.putClientProperty("JButton.segmentPosition", "middle"); + toolBar.add(captureLayersButton); + + invalidateButton = new JButton(); + invalidateButton.setAction(actionsMap.get(InvalidateAction.ACTION_NAME)); + invalidateButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + invalidateButton.putClientProperty("JButton.segmentPosition", "middle"); + toolBar.add(invalidateButton); + + requestLayoutButton = new JButton(); + requestLayoutButton.setAction(actionsMap.get(RequestLayoutAction.ACTION_NAME)); + requestLayoutButton.putClientProperty("JButton.buttonType", "segmentedTextured"); + requestLayoutButton.putClientProperty("JButton.segmentPosition", "last"); + toolBar.add(requestLayoutButton); + + return toolBar; + } + + private void setupProtocolDependentToolbar() { + // Some functionality is only enabled in certain versions of the protocol. + // Add/remove those buttons here + if (protocolVersion < 4) { + commandButtonsPanel.remove(dumpDisplayListButton); + } else if (dumpDisplayListButton.getParent() == null) { + commandButtonsPanel.add(dumpDisplayListButton, + commandButtonsPanel.getComponentCount() - 1); + } + } + + private JMenuBar buildMenuBar() { + JMenuBar menuBar = new JMenuBar(); + + JMenu fileMenu = new JMenu(); + JMenu viewMenu = new JMenu(); + JMenu viewHierarchyMenu = new JMenu(); + JMenu serverMenu = new JMenu(); + + saveMenuItem = new JMenuItem(); + JMenuItem exitMenuItem = new JMenuItem(); + + showDevicesMenuItem = new JMenuItem(); + + loadMenuItem = new JMenuItem(); + + startMenuItem = new JMenuItem(); + stopMenuItem = new JMenuItem(); + + fileMenu.setText("File"); + + saveMenuItem.setAction(actionsMap.get(SaveSceneAction.ACTION_NAME)); + fileMenu.add(saveMenuItem); + + exitMenuItem.setAction(actionsMap.get(ExitAction.ACTION_NAME)); + fileMenu.add(exitMenuItem); + + menuBar.add(fileMenu); + + viewMenu.setText("View"); + + showDevicesMenuItem.setAction(actionsMap.get(ShowDevicesAction.ACTION_NAME)); + showDevicesMenuItem.setEnabled(false); + viewMenu.add(showDevicesMenuItem); + + menuBar.add(viewMenu); + + viewHierarchyMenu.setText("Hierarchy"); + + loadMenuItem.setAction(actionsMap.get(LoadGraphAction.ACTION_NAME)); + viewHierarchyMenu.add(loadMenuItem); + + menuBar.add(viewHierarchyMenu); + + serverMenu.setText("Server"); + + startMenuItem.setAction(actionsMap.get(StartServerAction.ACTION_NAME)); + serverMenu.add(startMenuItem); + + stopMenuItem.setAction(actionsMap.get(StopServerAction.ACTION_NAME)); + serverMenu.add(stopMenuItem); + + menuBar.add(serverMenu); + + return menuBar; + } + + private JComponent buildPixelPerfectPanel() { + JSplitPane splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + + pixelPerfectTree = new JTree(new Object[0]); + pixelPerfectTree.setBorder(null); + pixelPerfectTree.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); + pixelPerfectTree.addTreeSelectionListener(new TreeSelectionListener() { + public void valueChanged(TreeSelectionEvent event) { + ViewNode node = (ViewNode) event.getPath().getLastPathComponent(); + screenViewer.select(node); + } + }); + + JScrollPane scroller = new JScrollPane(pixelPerfectTree); + scroller.setBorder(null); + scroller.getViewport().setBorder(null); + + splitter.setContinuousLayout(true); + splitter.setLeftComponent(scroller); + splitter.setRightComponent(buildPixelPerfectViewer(splitter)); + splitter.setBorder(null); + + if (OS.isMacOsX() && OS.isLeopardOrLater()) { + splitter.setBorder(new UnifiedContentBorder()); + } + + return splitter; + } + + private JComponent buildPixelPerfectViewer(JSplitPane splitter) { + screenViewer = new ScreenViewer(this, currentDevice, splitter.getDividerSize()); + return screenViewer; + } + + private void toggleGraphView() { + showStatusBarComponents(); + + screenViewer.stop(); + mainPanel.remove(pixelPerfectPanel); + mainPanel.add(mainSplitter, BorderLayout.CENTER); + + validate(); + repaint(); + } + + private void showStatusBarComponents() { + viewCountLabel.setVisible(true); + zoomSlider.setVisible(true); + minZoomLabel.setVisible(true); + maxZoomLabel.setVisible(true); + filterLabel.setVisible(true); + filterText.setVisible(true); + } + + private void togglePixelPerfectView() { + if (pixelPerfectPanel == null) { + pixelPerfectPanel = buildPixelPerfectPanel(); + showPixelPerfectTree(); + } else { + screenViewer.start(); + } + + hideStatusBarComponents(); + + mainPanel.remove(mainSplitter); + mainPanel.add(pixelPerfectPanel, BorderLayout.CENTER); + + validate(); + repaint(); + } + + private void zoomSliderStateChanged(ChangeEvent evt) { + JSlider slider = (JSlider) evt.getSource(); + if (sceneView != null) { + scene.setZoomFactor(slider.getValue() / 100.0d); + sceneView.repaint(); + } + } + + private void showProperties(ViewNode node) { + propertiesTable.setModel(new PropertiesTableModel(node)); + } + + private void updateProfiles(double[] profiles) { + profilingTable.setModel(new ProfilesTableModel(profiles)); + setVisibleRowCount(profilingTable, profiles.length + 1); + } + + public static void setVisibleRowCount(JTable table, int rows) { + int height = 0; + for (int row = 0; row < rows; row++) { + height += table.getRowHeight(row); + } + + Dimension size = new Dimension(table.getPreferredScrollableViewportSize().width, height); + table.setPreferredScrollableViewportSize(size); + table.revalidate(); + } + + private void showPixelPerfectTree() { + if (pixelPerfectTree == null) { + return; + } + pixelPerfectTree.setModel(new ViewsTreeModel(scene.getRoot())); + pixelPerfectTree.setCellRenderer(new ViewsTreeCellRenderer()); + expandAll(pixelPerfectTree, true); + + } + + private static void expandAll(JTree tree, boolean expand) { + ViewNode root = (ViewNode) tree.getModel().getRoot(); + expandAll(tree, new TreePath(root), expand); + } + + private static void expandAll(JTree tree, TreePath parent, boolean expand) { + // Traverse children + ViewNode node = (ViewNode)parent.getLastPathComponent(); + if (node.children != null) { + for (ViewNode n : node.children) { + TreePath path = parent.pathByAddingChild(n); + expandAll(tree, path, expand); + } + } + + if (expand) { + tree.expandPath(parent); + } else { + tree.collapsePath(parent); + } + } + + private void createGraph(ViewHierarchyScene scene) { + scene.addObjectSceneListener(new SceneFocusListener(), + ObjectSceneEventType.OBJECT_FOCUS_CHANGED); + + if (mainSplitter == null) { + mainPanel.remove(deviceSelector); + mainPanel.add(buildGraphPanel(), BorderLayout.CENTER); + showDevicesButton.setEnabled(true); + showDevicesMenuItem.setEnabled(true); + graphViewButton.setEnabled(true); + pixelPerfectViewButton.setEnabled(true); + + showStatusBarComponents(); + } + + sceneView = scene.createView(); + sceneView.addMouseListener(new NodeClickListener()); + sceneView.addMouseWheelListener(new WheelZoomListener()); + sceneScroller.setViewportView(sceneView); + + if (extrasPanel != null) { + sideSplitter.remove(extrasPanel); + } + sideSplitter.setBottomComponent(buildExtrasPanel()); + + mainSplitter.setDividerLocation(getWidth() - mainSplitter.getDividerSize() - + buttonsPanel.getPreferredSize().width); + + captureLayersButton.setEnabled(true); + saveMenuItem.setEnabled(true); + showPixelPerfectTree(); + + updateStatus(); + layoutScene(); + } + + private void layoutScene() { + TreeGraphLayout<ViewNode, String> layout = + new TreeGraphLayout<ViewNode, String>(scene, 50, 50, 70, 30, true); + layout.layout(scene.getRoot()); + } + + private void updateStatus() { + viewCountLabel.setText("" + scene.getNodes().size() + " views"); + zoomSlider.setEnabled(scene.getNodes().size() > 0); + } + + private JPanel buildExtrasPanel() { + extrasPanel = new JPanel(new BorderLayout()); + JScrollPane p = new JScrollPane(layoutView = new LayoutRenderer(scene, sceneView)); + JScrollBar b = p.getVerticalScrollBar(); + b.setUnitIncrement(10); + extrasPanel.add(p); + extrasPanel.add(scene.createSatelliteView(), BorderLayout.SOUTH); + extrasPanel.add(buildLayoutViewControlButtons(), BorderLayout.NORTH); + return extrasPanel; + } + + private JComponent buildLayoutViewControlButtons() { + buttonsPanel = new JToolBar(); + buttonsPanel.setFloatable(false); + + ButtonGroup group = new ButtonGroup(); + + JToggleButton white = new JToggleButton("On White"); + toggleColorOnSelect(white); + white.putClientProperty("JButton.buttonType", "segmentedTextured"); + white.putClientProperty("JButton.segmentPosition", "first"); + white.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + layoutView.setBackground(Color.WHITE); + layoutView.setForeground(Color.BLACK); + } + }); + group.add(white); + buttonsPanel.add(white); + + JToggleButton black = new JToggleButton("On Black"); + toggleColorOnSelect(black); + black.putClientProperty("JButton.buttonType", "segmentedTextured"); + black.putClientProperty("JButton.segmentPosition", "last"); + black.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + layoutView.setBackground(Color.BLACK); + layoutView.setForeground(Color.WHITE); + } + }); + group.add(black); + buttonsPanel.add(black); + + black.setSelected(true); + + JCheckBox showExtras = new JCheckBox("Show Extras"); + showExtras.putClientProperty("JComponent.sizeVariant", "small"); + showExtras.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + layoutView.setShowExtras(((JCheckBox) e.getSource()).isSelected()); + } + }); + buttonsPanel.add(showExtras); + + return buttonsPanel; + } + + private void showCaptureWindow(ViewNode node, String captureParams, Image image) { + if (image != null) { + layoutView.repaint(); + + JFrame frame = new JFrame(captureParams); + JPanel panel = new JPanel(new BorderLayout()); + + final CaptureRenderer label = new CaptureRenderer(new ImageIcon(image), node); + label.setBorder(BorderFactory.createEmptyBorder(24, 24, 24, 24)); + + final JPanel solidColor = new JPanel(new BorderLayout()); + solidColor.setBackground(Color.BLACK); + solidColor.add(label); + + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + + ButtonGroup group = new ButtonGroup(); + + JToggleButton white = new JToggleButton("On White"); + toggleColorOnSelect(white); + white.putClientProperty("JButton.buttonType", "segmentedTextured"); + white.putClientProperty("JButton.segmentPosition", "first"); + white.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + solidColor.setBackground(Color.WHITE); + } + }); + group.add(white); + toolBar.add(white); + + JToggleButton black = new JToggleButton("On Black"); + toggleColorOnSelect(black); + black.putClientProperty("JButton.buttonType", "segmentedTextured"); + black.putClientProperty("JButton.segmentPosition", "last"); + black.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + solidColor.setBackground(Color.BLACK); + } + }); + group.add(black); + toolBar.add(black); + + black.setSelected(true); + + JCheckBox showExtras = new JCheckBox("Show Extras"); + showExtras.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + label.setShowExtras(((JCheckBox) e.getSource()).isSelected()); + } + }); + toolBar.add(showExtras); + + panel.add(toolBar, BorderLayout.NORTH); + panel.add(solidColor); + frame.add(panel); + + frame.pack(); + frame.setResizable(false); + frame.setLocationRelativeTo(Workspace.this); + frame.setVisible(true); + } + } + + private void reset() { + currentDevice = null; + currentWindow = null; + currentDeviceChanged(); + windowsTableModel.setVisible(false); + windowsTableModel.clear(); + + showDevicesSelector(); + } + + public void showDevicesSelector() { + if (mainSplitter != null) { + if (pixelPerfectPanel != null) { + screenViewer.start(); + } + mainPanel.remove(graphViewButton.isSelected() ? mainSplitter : pixelPerfectPanel); + mainPanel.add(deviceSelector, BorderLayout.CENTER); + pixelPerfectPanel = mainSplitter = null; + graphViewButton.setSelected(true); + + hideStatusBarComponents(); + + saveMenuItem.setEnabled(false); + showDevicesMenuItem.setEnabled(false); + showDevicesButton.setEnabled(false); + displayNodeButton.setEnabled(false); + captureLayersButton.setEnabled(false); + invalidateButton.setEnabled(false); + dumpDisplayListButton.setEnabled(false); + requestLayoutButton.setEnabled(false); + graphViewButton.setEnabled(false); + pixelPerfectViewButton.setEnabled(false); + + if (currentDevice != null) { + if (!DeviceBridge.isViewServerRunning(currentDevice)) { + DeviceBridge.startViewServer(currentDevice); + } + loadWindows().execute(); + windowsTableModel.setVisible(true); + } + + validate(); + repaint(); + } + } + + private void currentDeviceChanged() { + if (currentDevice == null) { + startButton.setEnabled(false); + startMenuItem.setEnabled(false); + stopButton.setEnabled(false); + stopMenuItem.setEnabled(false); + refreshButton.setEnabled(false); + saveMenuItem.setEnabled(false); + loadButton.setEnabled(false); + displayNodeButton.setEnabled(false); + captureLayersButton.setEnabled(false); + invalidateButton.setEnabled(false); + dumpDisplayListButton.setEnabled(false); + graphViewButton.setEnabled(false); + pixelPerfectViewButton.setEnabled(false); + requestLayoutButton.setEnabled(false); + loadMenuItem.setEnabled(false); + } else { + loadMenuItem.setEnabled(true); + checkForServerOnCurrentDevice(); + } + } + + private void checkForServerOnCurrentDevice() { + if (DeviceBridge.isViewServerRunning(currentDevice)) { + startButton.setEnabled(false); + startMenuItem.setEnabled(false); + stopButton.setEnabled(true); + stopMenuItem.setEnabled(true); + loadButton.setEnabled(true); + refreshButton.setEnabled(true); + } else { + startButton.setEnabled(true); + startMenuItem.setEnabled(true); + stopButton.setEnabled(false); + stopMenuItem.setEnabled(false); + loadButton.setEnabled(false); + refreshButton.setEnabled(false); + } + } + + public void cleanupDevices() { + for (IDevice device : devicesTableModel.getDevices()) { + DeviceBridge.removeDeviceForward(device); + } + } + + private static void toggleColorOnSelect(JToggleButton button) { + if (!OS.isMacOsX() || !OS.isLeopardOrLater()) { + return; + } + + button.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent event) { + JToggleButton button = (JToggleButton) event.getSource(); + if (button.isSelected()) { + button.setForeground(Color.WHITE); + } else { + button.setForeground(Color.BLACK); + } + } + }); + } + + private void updateFilter(DocumentEvent e) { + final Document document = e.getDocument(); + try { + updateFilteredNodes(document.getText(0, document.getLength())); + } catch (BadLocationException e1) { + e1.printStackTrace(); + } + } + + private void updateFilteredNodes(String filterText) { + final ViewNode root = scene.getRoot(); + try { + final Pattern pattern = Pattern.compile(filterText, Pattern.CASE_INSENSITIVE); + filterNodes(pattern, root); + } catch (PatternSyntaxException e) { + filterNodes(null, root); + } + repaint(); + } + + private void filterNodes(Pattern pattern, ViewNode root) { + root.filter(pattern); + + for (ViewNode node : root.children) { + filterNodes(pattern, node); + } + } + + public void beginTask() { + progress.setVisible(true); + } + + public void endTask() { + progress.setVisible(false); + } + + public SwingWorker<?, ?> showNodeCapture() { + if (scene.getFocusedObject() == null) { + return null; + } + return new CaptureNodeTask(); + } + + public SwingWorker<?, ?> outputDisplayList() { + if (scene.getFocusedObject() == null) { + return null; + } + return new DumpDisplayListTask(); + } + + public SwingWorker<?, ?> captureLayers() { + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(new PsdFileFilter()); + int choice = chooser.showSaveDialog(sceneView); + if (choice == JFileChooser.APPROVE_OPTION) { + return new CaptureLayersTask(chooser.getSelectedFile()); + } else { + return null; + } + } + + public SwingWorker<?, ?> startServer() { + return new StartServerTask(); + } + + public SwingWorker<?, ?> stopServer() { + return new StopServerTask(); + } + + public SwingWorker<?, ?> loadWindows() { + return new LoadWindowsTask(); + } + + public SwingWorker<?, ?> loadGraph() { + return new LoadGraphTask(); + } + + public SwingWorker<?, ?> invalidateView() { + if (scene.getFocusedObject() == null) { + return null; + } + return new InvalidateTask(); + } + + public SwingWorker<?, ?> requestLayout() { + if (scene.getFocusedObject() == null) { + return null; + } + return new RequestLayoutTask(); + } + + public SwingWorker<?, ?> saveSceneAsImage() { + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(new PngFileFilter()); + int choice = chooser.showSaveDialog(sceneView); + if (choice == JFileChooser.APPROVE_OPTION) { + return new SaveSceneTask(chooser.getSelectedFile()); + } else { + return null; + } + } + + private class InvalidateTask extends SwingWorker<Object, Void> { + private String captureParams; + + private InvalidateTask() { + captureParams = scene.getFocusedObject().toString(); + beginTask(); + } + + @Override + @WorkerThread + protected Object doInBackground() throws Exception { + ViewManager.invalidate(currentDevice, currentWindow, captureParams); + return null; + } + + @Override + protected void done() { + endTask(); + } + } + + private class DumpDisplayListTask extends SwingWorker<Object, Void> { + private String captureParams; + + private DumpDisplayListTask() { + captureParams = scene.getFocusedObject().toString(); + beginTask(); + } + + @Override + @WorkerThread + protected Object doInBackground() throws Exception { + ViewManager.outputDisplayList(currentDevice, currentWindow, captureParams); + return null; + } + + @Override + protected void done() { + endTask(); + } + } + + private class RequestLayoutTask extends SwingWorker<Object, Void> { + private String captureParams; + + private RequestLayoutTask() { + captureParams = scene.getFocusedObject().toString(); + beginTask(); + } + + @Override + @WorkerThread + protected Object doInBackground() throws Exception { + ViewManager.requestLayout(currentDevice, currentWindow, captureParams); + return null; + } + + @Override + protected void done() { + endTask(); + } + } + + private class CaptureLayersTask extends SwingWorker<Boolean, Void> { + private File file; + + private CaptureLayersTask(File file) { + this.file = file; + beginTask(); + } + + @Override + @WorkerThread + protected Boolean doInBackground() throws Exception { + return CaptureLoader.saveLayers(currentDevice, currentWindow, file); + } + + @Override + protected void done() { + endTask(); + } + } + + private class CaptureNodeTask extends SwingWorker<Image, Void> { + private String captureParams; + private ViewNode node; + + private CaptureNodeTask() { + node = (ViewNode) scene.getFocusedObject(); + captureParams = node.toString(); + beginTask(); + } + + @Override + @WorkerThread + protected Image doInBackground() throws Exception { + node.image = CaptureLoader.loadCapture(currentDevice, currentWindow, captureParams); + return node.image; + } + + @Override + protected void done() { + try { + Image image = get(); + showCaptureWindow(node, captureParams, image); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } finally { + endTask(); + } + } + } + + static class WindowsResult { + Window[] windows; + int serverVersion; + int protocolVersion; + } + + private class LoadWindowsTask extends SwingWorker<WindowsResult, Void> { + private LoadWindowsTask() { + beginTask(); + } + + @Override + @WorkerThread + protected WindowsResult doInBackground() throws Exception { + WindowsResult r = new WindowsResult(); + r.protocolVersion = VersionLoader.loadProtocolVersion(currentDevice); + r.serverVersion = VersionLoader.loadServerVersion(currentDevice); + r.windows = WindowsLoader.loadWindows(currentDevice, + r.protocolVersion, r.serverVersion); + return r; + } + + @Override + protected void done() { + try { + WindowsResult result = get(); + protocolVersion = result.protocolVersion; + serverVersion = result.serverVersion; + setupProtocolDependentToolbar(); + windowsTableModel.clear(); + windowsTableModel.addWindows(result.windows); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + endTask(); + } + } + } + + private class StartServerTask extends SwingWorker<Object, Void> { + public StartServerTask() { + beginTask(); + } + + @Override + @WorkerThread + protected Object doInBackground() { + DeviceBridge.startViewServer(currentDevice); + return null; + } + + @Override + protected void done() { + new LoadWindowsTask().execute(); + windowsTableModel.setVisible(true); + checkForServerOnCurrentDevice(); + endTask(); + } + } + + private class StopServerTask extends SwingWorker<Object, Void> { + public StopServerTask() { + beginTask(); + } + + @Override + @WorkerThread + protected Object doInBackground() { + DeviceBridge.stopViewServer(currentDevice); + return null; + } + + @Override + protected void done() { + windowsTableModel.setVisible(false); + windowsTableModel.clear(); + checkForServerOnCurrentDevice(); + endTask(); + } + } + + private class LoadGraphTask extends SwingWorker<double[], Void> { + public LoadGraphTask() { + beginTask(); + } + + @Override + @WorkerThread + protected double[] doInBackground() { + scene = ViewHierarchyLoader.loadScene(currentDevice, currentWindow); + return ProfilesLoader.loadProfiles(currentDevice, currentWindow, + scene.getRoot().toString()); + } + + @Override + protected void done() { + try { + createGraph(scene); + updateProfiles(get()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } finally { + endTask(); + } + } + } + + private class SaveSceneTask extends SwingWorker<Object, Void> { + private File file; + + private SaveSceneTask(File file) { + this.file = file; + beginTask(); + } + + @Override + @WorkerThread + protected Object doInBackground() { + if (sceneView == null) { + return null; + } + + try { + BufferedImage image = new BufferedImage(sceneView.getWidth(), + sceneView.getHeight(), BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = image.createGraphics(); + sceneView.paint(g2); + g2.dispose(); + ImageIO.write(image, "PNG", file); + } catch (IOException ex) { + ex.printStackTrace(); + } + return null; + } + + @Override + protected void done() { + endTask(); + } + } + + private class SceneFocusListener implements ObjectSceneListener { + + public void objectAdded(ObjectSceneEvent arg0, Object arg1) { + } + + public void objectRemoved(ObjectSceneEvent arg0, Object arg1) { + } + + public void objectStateChanged(ObjectSceneEvent arg0, Object arg1, + ObjectState arg2, ObjectState arg3) { + } + + public void selectionChanged(ObjectSceneEvent e, Set<Object> previousSelection, + Set<Object> newSelection) { + } + + public void highlightingChanged(ObjectSceneEvent arg0, Set<Object> arg1, Set<Object> arg2) { + } + + public void hoverChanged(ObjectSceneEvent arg0, Object arg1, Object arg2) { + } + + public void focusChanged(ObjectSceneEvent e, Object oldFocus, Object newFocus) { + displayNodeButton.setEnabled(true); + invalidateButton.setEnabled(true); + dumpDisplayListButton.setEnabled(true); + requestLayoutButton.setEnabled(true); + + Set<Object> selection = new HashSet<Object>(); + selection.add(newFocus); + scene.setSelectedObjects(selection); + + showProperties((ViewNode) newFocus); + layoutView.repaint(); + } + } + + private class NodeClickListener extends MouseAdapter { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + showNodeCapture().execute(); + } + } + } + + private class WheelZoomListener implements MouseWheelListener { + public void mouseWheelMoved(MouseWheelEvent e) { + if (zoomSlider != null) { + int val = zoomSlider.getValue(); + val -= e.getWheelRotation() * 10; + zoomSlider.setValue(val); + } + } + } + private class DevicesTableModel extends DefaultTableModel implements + AndroidDebugBridge.IDeviceChangeListener { + + private ArrayList<IDevice> devices; + + private DevicesTableModel() { + devices = new ArrayList<IDevice>(); + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + + @Override + public Object getValueAt(int row, int column) { + return devices.get(row); + } + + @Override + public String getColumnName(int column) { + return "Devices"; + } + + @WorkerThread + public void deviceConnected(final IDevice device) { + DeviceBridge.setupDeviceForward(device); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + addDevice(device); + } + }); + } + + @WorkerThread + public void deviceDisconnected(final IDevice device) { + DeviceBridge.removeDeviceForward(device); + + SwingUtilities.invokeLater(new Runnable() { + public void run() { + removeDevice(device); + } + }); + } + + public void addDevice(IDevice device) { + if (!devices.contains(device)) { + devices.add(device); + fireTableDataChanged(); + } + } + + public void removeDevice(IDevice device) { + if (device.equals(currentDevice)) { + reset(); + } + + if (devices.contains(device)) { + devices.remove(device); + fireTableDataChanged(); + } + } + + @WorkerThread + public void deviceChanged(IDevice device, int changeMask) { + if ((changeMask & IDevice.CHANGE_STATE) != 0 && + device.isOnline()) { + // if the device state changed and it's now online, we set up its port forwarding. + DeviceBridge.setupDeviceForward(device); + } else if (device == currentDevice && (changeMask & IDevice.CHANGE_CLIENT_LIST) != 0) { + // if the changed device is the current one and the client list changed, we update + // the UI. + loadWindows().execute(); + windowsTableModel.setVisible(true); + } + } + + @Override + public int getRowCount() { + return devices == null ? 0 : devices.size(); + } + + public IDevice getDevice(int index) { + return index < devices.size() ? devices.get(index) : null; + } + + public IDevice[] getDevices() { + return devices.toArray(new IDevice[devices.size()]); + } + } + + private static class WindowsTableModel extends DefaultTableModel { + private ArrayList<Window> windows; + private boolean visible; + + private WindowsTableModel() { + windows = new ArrayList<Window>(); + windows.add(Window.FOCUSED_WINDOW); + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + + @Override + public String getColumnName(int column) { + return "Windows"; + } + + @Override + public Object getValueAt(int row, int column) { + return windows.get(row); + } + + @Override + public int getRowCount() { + return !visible || windows == null ? 0 : windows.size(); + } + + public void setVisible(boolean visible) { + this.visible = visible; + fireTableDataChanged(); + } + + public void addWindow(Window window) { + windows.add(window); + fireTableDataChanged(); + } + + public void addWindows(Window[] windowsList) { + //noinspection ManualArrayToCollectionCopy + for (Window window : windowsList) { + windows.add(window); + } + fireTableDataChanged(); + } + + public void clear() { + windows.clear(); + windows.add(Window.FOCUSED_WINDOW); + } + + public Window getWindow(int index) { + return windows.get(index); + } + } + + private class DeviceSelectedListener implements ListSelectionListener { + public void valueChanged(ListSelectionEvent event) { + if (event.getValueIsAdjusting()) { + return; + } + + int row = devices.getSelectedRow(); + if (row >= 0) { + currentDevice = devicesTableModel.getDevice(row); + currentDeviceChanged(); + if (currentDevice != null) { + if (!DeviceBridge.isViewServerRunning(currentDevice)) { + DeviceBridge.startViewServer(currentDevice); + checkForServerOnCurrentDevice(); + } + loadWindows().execute(); + windowsTableModel.setVisible(true); + } + } else { + currentDevice = null; + currentDeviceChanged(); + windowsTableModel.setVisible(false); + windowsTableModel.clear(); + } + } + } + + private class WindowSelectedListener implements ListSelectionListener { + public void valueChanged(ListSelectionEvent event) { + if (event.getValueIsAdjusting()) { + return; + } + + int row = windows.getSelectedRow(); + if (row >= 0) { + currentWindow = windowsTableModel.getWindow(row); + } else { + currentWindow = Window.FOCUSED_WINDOW; + } + } + } + + private static class ViewsTreeCellRenderer extends DefaultTreeCellRenderer { + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, + boolean expanded, boolean leaf, int row, boolean hasFocus) { + + final String name = ((ViewNode) value).name; + value = name.substring(name.lastIndexOf('.') + 1, name.lastIndexOf('@')); + return super.getTreeCellRendererComponent(tree, value, selected, expanded, + leaf, row, hasFocus); + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/BackgroundAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/BackgroundAction.java new file mode 100644 index 000000000..b2046fd81 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/BackgroundAction.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import javax.swing.AbstractAction; +import javax.swing.SwingWorker; + +public abstract class BackgroundAction extends AbstractAction { + protected void executeBackgroundTask(SwingWorker<?, ?> worker) { + if (worker != null) { + worker.execute(); + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureLayersAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureLayersAction.java new file mode 100644 index 000000000..2fff04179 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureLayersAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class CaptureLayersAction extends BackgroundAction { + public static final String ACTION_NAME = "captureLayers"; + private Workspace mWorkspace; + + public CaptureLayersAction(Workspace workspace) { + putValue(NAME, "Capture PSD"); + putValue(SHORT_DESCRIPTION, "Capture PSD"); + putValue(LONG_DESCRIPTION, "Capture current window into a Photoshop PSD file"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_P, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.captureLayers()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureNodeAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureNodeAction.java new file mode 100644 index 000000000..a8aee3c01 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureNodeAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class CaptureNodeAction extends BackgroundAction { + public static final String ACTION_NAME = "captureNode"; + private Workspace mWorkspace; + + public CaptureNodeAction(Workspace workspace) { + putValue(NAME, "Display View"); + putValue(SHORT_DESCRIPTION, "Display View"); + putValue(LONG_DESCRIPTION, "Display View"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_D, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.showNodeCapture()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/DumpDisplayListAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/DumpDisplayListAction.java new file mode 100644 index 000000000..3e667946b --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/DumpDisplayListAction.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2011 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class DumpDisplayListAction extends BackgroundAction { + public static final String ACTION_NAME = "dumpDisplayList"; + private Workspace mWorkspace; + + public DumpDisplayListAction(Workspace workspace) { + putValue(NAME, "Dump DisplayList"); + putValue(SHORT_DESCRIPTION, "Dump DisplayList"); + putValue(LONG_DESCRIPTION, "Dump DisplayList"); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.outputDisplayList()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ExitAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ExitAction.java new file mode 100644 index 000000000..e5aaed555 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ExitAction.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; +import com.android.hierarchyviewer.device.DeviceBridge; + +import javax.swing.AbstractAction; +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class ExitAction extends AbstractAction { + public static final String ACTION_NAME = "exit"; + private Workspace mWorkspace; + + public ExitAction(Workspace workspace) { + putValue(NAME, "Quit"); + putValue(SHORT_DESCRIPTION, "Quit"); + putValue(LONG_DESCRIPTION, "Quit"); + putValue(MNEMONIC_KEY, KeyEvent.VK_Q); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + mWorkspace.cleanupDevices(); + mWorkspace.dispose(); + DeviceBridge.terminate(); + System.exit(0); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/InvalidateAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/InvalidateAction.java new file mode 100644 index 000000000..7767cda74 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/InvalidateAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class InvalidateAction extends BackgroundAction { + public static final String ACTION_NAME = "invalidate"; + private Workspace mWorkspace; + + public InvalidateAction(Workspace workspace) { + putValue(NAME, "Invalidate"); + putValue(SHORT_DESCRIPTION, "Invalidate"); + putValue(LONG_DESCRIPTION, "Invalidate"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_I, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.invalidateView()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/LoadGraphAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/LoadGraphAction.java new file mode 100644 index 000000000..42c8b8e60 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/LoadGraphAction.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class LoadGraphAction extends BackgroundAction { + public static final String ACTION_NAME = "loadGraph"; + private Workspace mWorkspace; + + public LoadGraphAction(Workspace workspace) { + putValue(NAME, "Load View Hierarchy"); + putValue(SHORT_DESCRIPTION, "Load"); + putValue(LONG_DESCRIPTION, "Load View Hierarchy"); + putValue(MNEMONIC_KEY, KeyEvent.VK_L); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_L, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.loadGraph()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RefreshWindowsAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RefreshWindowsAction.java new file mode 100644 index 000000000..bcb7ea73c --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RefreshWindowsAction.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; + +public class RefreshWindowsAction extends BackgroundAction { + public static final String ACTION_NAME = "refreshWindows"; + private Workspace mWorkspace; + + public RefreshWindowsAction(Workspace workspace) { + putValue(NAME, "Refresh Windows"); + putValue(SHORT_DESCRIPTION, "Refresh"); + putValue(LONG_DESCRIPTION, "Refresh Windows"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F7, 0)); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.loadWindows()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RequestLayoutAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RequestLayoutAction.java new file mode 100644 index 000000000..6fc2832df --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RequestLayoutAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class RequestLayoutAction extends BackgroundAction { + public static final String ACTION_NAME = "requestLayout"; + private Workspace mWorkspace; + + public RequestLayoutAction(Workspace workspace) { + putValue(NAME, "Request Layout"); + putValue(SHORT_DESCRIPTION, "Request Layout"); + putValue(LONG_DESCRIPTION, "Request Layout"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_R, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.requestLayout()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/SaveSceneAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/SaveSceneAction.java new file mode 100644 index 000000000..7c7a8a9e2 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/SaveSceneAction.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class SaveSceneAction extends BackgroundAction { + public static final String ACTION_NAME = "saveScene"; + private Workspace mWorkspace; + + public SaveSceneAction(Workspace workspace) { + mWorkspace = workspace; + putValue(NAME, "Save as PNG..."); + putValue(SHORT_DESCRIPTION, "Save"); + putValue(LONG_DESCRIPTION, "Save as PNG..."); + putValue(MNEMONIC_KEY, KeyEvent.VK_S); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.saveSceneAsImage()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ShowDevicesAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ShowDevicesAction.java new file mode 100644 index 000000000..a91ab7a00 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ShowDevicesAction.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.AbstractAction; +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; +import java.awt.Toolkit; + +public class ShowDevicesAction extends AbstractAction { + public static final String ACTION_NAME = "showDevices"; + private Workspace mWorkspace; + + public ShowDevicesAction(Workspace workspace) { + putValue(NAME, "Devices"); + putValue(SHORT_DESCRIPTION, "Devices"); + putValue(LONG_DESCRIPTION, "Show Devices"); + putValue(MNEMONIC_KEY, KeyEvent.VK_D); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_D, + Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + mWorkspace.showDevicesSelector(); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StartServerAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StartServerAction.java new file mode 100644 index 000000000..ccb6ae02c --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StartServerAction.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; + +public class StartServerAction extends BackgroundAction { + public static final String ACTION_NAME = "startServer"; + private Workspace mWorkspace; + + public StartServerAction(Workspace workspace) { + putValue(NAME, "Start Server"); + putValue(SHORT_DESCRIPTION, "Start"); + putValue(LONG_DESCRIPTION, "Start Server"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0)); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.startServer()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StopServerAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StopServerAction.java new file mode 100644 index 000000000..ac76e14ff --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StopServerAction.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.action; + +import com.android.hierarchyviewer.ui.Workspace; + +import javax.swing.KeyStroke; +import java.awt.event.KeyEvent; +import java.awt.event.ActionEvent; + +public class StopServerAction extends BackgroundAction { + public static final String ACTION_NAME = "stopServer"; + private Workspace mWorkspace; + + public StopServerAction(Workspace workspace) { + putValue(NAME, "Stop Server"); + putValue(SHORT_DESCRIPTION, "Stop"); + putValue(LONG_DESCRIPTION, "Stop Server"); + putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F6, 0)); + this.mWorkspace = workspace; + } + + public void actionPerformed(ActionEvent e) { + executeBackgroundTask(mWorkspace.stopServer()); + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java new file mode 100644 index 000000000..fcbe6b57e --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ProfilesTableModel.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2009 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.android.hierarchyviewer.ui.model; + +import javax.swing.table.DefaultTableModel; +import java.text.NumberFormat; + +public class ProfilesTableModel extends DefaultTableModel { + private static final String[] NAMES = { "measure", "layout", "draw" }; + + private final double[] profiles; + private final NumberFormat formatter; + + public ProfilesTableModel(double[] profiles) { + this.profiles = profiles; + formatter = NumberFormat.getNumberInstance(); + } + + @Override + public int getRowCount() { + return profiles == null ? 0 : profiles.length; + } + + @Override + public Object getValueAt(int row, int column) { + if (profiles == null) return ""; + + if (column == 0) { + return NAMES[row]; + } + + + return formatter.format(profiles[row]) + ""; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public String getColumnName(int column) { + return column == 0 ? "Operation" : "Duration (ms)"; + } + + @Override + public boolean isCellEditable(int arg0, int arg1) { + return false; + } + + @Override + public void setValueAt(Object arg0, int arg1, int arg2) { + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/PropertiesTableModel.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/PropertiesTableModel.java new file mode 100644 index 000000000..cc4f7e38f --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/PropertiesTableModel.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.model; + +import com.android.hierarchyviewer.scene.ViewNode; + +import javax.swing.table.DefaultTableModel; +import java.util.List; +import java.util.ArrayList; + +public class PropertiesTableModel extends DefaultTableModel { + private List<ViewNode.Property> properties; + private List<ViewNode.Property> privateProperties = new ArrayList<ViewNode.Property>(); + + public PropertiesTableModel(ViewNode node) { + properties = node.properties; + loadPrivateProperties(node); + } + + private void loadPrivateProperties(ViewNode node) { + int x = node.left; + int y = node.top; + ViewNode p = node.parent; + while (p != null) { + x += p.left - p.scrollX; + y += p.top - p.scrollY; + p = p.parent; + } + + ViewNode.Property property = new ViewNode.Property(); + property.name = "absolute_x"; + property.value = String.valueOf(x); + privateProperties.add(property); + + property = new ViewNode.Property(); + property.name = "absolute_y"; + property.value = String.valueOf(y); + privateProperties.add(property); + } + + @Override + public int getRowCount() { + return (privateProperties == null ? 0 : privateProperties.size()) + + (properties == null ? 0 : properties.size()); + } + + @Override + public Object getValueAt(int row, int column) { + ViewNode.Property property; + + if (row < privateProperties.size()) { + property = privateProperties.get(row); + } else { + property = properties.get(row - privateProperties.size()); + } + + return column == 0 ? property.name : property.value; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public String getColumnName(int column) { + return column == 0 ? "Property" : "Value"; + } + + @Override + public boolean isCellEditable(int arg0, int arg1) { + return false; + } + + @Override + public void setValueAt(Object arg0, int arg1, int arg2) { + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ViewsTreeModel.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ViewsTreeModel.java new file mode 100644 index 000000000..f3a07f837 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ViewsTreeModel.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.model; + +import com.android.hierarchyviewer.scene.ViewNode; + +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.event.TreeModelListener; + +public class ViewsTreeModel implements TreeModel { + private final ViewNode root; + + public ViewsTreeModel(ViewNode root) { + this.root = root; + } + + public Object getRoot() { + return root; + } + + public Object getChild(Object o, int i) { + return ((ViewNode) o).children.get(i); + } + + public int getChildCount(Object o) { + return ((ViewNode) o).children.size(); + } + + public boolean isLeaf(Object child) { + ViewNode node = (ViewNode) child; + return node.children == null || node.children.size() == 0; + } + + public void valueForPathChanged(TreePath treePath, Object child) { + } + + public int getIndexOfChild(Object parent, Object child) { + //noinspection SuspiciousMethodCalls + return ((ViewNode) parent).children.indexOf(child); + } + + public void addTreeModelListener(TreeModelListener treeModelListener) { + } + + public void removeTreeModelListener(TreeModelListener treeModelListener) { + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/IconLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/IconLoader.java new file mode 100644 index 000000000..ef7395647 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/IconLoader.java @@ -0,0 +1,49 @@ +package com.android.hierarchyviewer.ui.util; + +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.imageio.ImageIO; +import java.io.IOException; +import java.awt.image.BufferedImage; +import java.awt.Graphics; +import java.awt.GraphicsEnvironment; +import java.awt.GraphicsConfiguration; + +public class IconLoader { + public static Icon load(Class<?> klass, String path) { + try { + return new ImageIcon(ImageIO.read(klass.getResourceAsStream(path))); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private static GraphicsConfiguration getGraphicsConfiguration() { + GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment(); + return environment.getDefaultScreenDevice().getDefaultConfiguration(); + } + + private static boolean isHeadless() { + return GraphicsEnvironment.isHeadless(); + } + + public static BufferedImage toCompatibleImage(BufferedImage image) { + if (isHeadless()) { + return image; + } + + if (image.getColorModel().equals( + getGraphicsConfiguration().getColorModel())) { + return image; + } + + BufferedImage compatibleImage = getGraphicsConfiguration().createCompatibleImage( + image.getWidth(), image.getHeight(), image.getTransparency()); + Graphics g = compatibleImage.getGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + + return compatibleImage; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PngFileFilter.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PngFileFilter.java new file mode 100644 index 000000000..5d6472dcf --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PngFileFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.util; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +public class PngFileFilter extends FileFilter { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".png"); + } + + @Override + public String getDescription() { + return "PNG Image (*.png)"; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PsdFile.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PsdFile.java new file mode 100644 index 000000000..3768e41b5 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PsdFile.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2010 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.android.hierarchyviewer.ui.util; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes PSD file. + * + * Supports only 8 bits, RGB images with 4 channels. + */ +public class PsdFile { + private final Header mHeader; + private final ColorMode mColorMode; + private final ImageResources mImageResources; + private final LayersMasksInfo mLayersMasksInfo; + private final LayersInfo mLayersInfo; + + private final BufferedImage mMergedImage; + private final Graphics2D mGraphics; + + public PsdFile(int width, int height) { + mHeader = new Header(width, height); + mColorMode = new ColorMode(); + mImageResources = new ImageResources(); + mLayersMasksInfo = new LayersMasksInfo(); + mLayersInfo = new LayersInfo(); + + mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + mGraphics = mMergedImage.createGraphics(); + } + + public void addLayer(String name, BufferedImage image, Point offset) { + addLayer(name, image, offset, true); + } + + public void addLayer(String name, BufferedImage image, Point offset, boolean visible) { + mLayersInfo.addLayer(name, image, offset, visible); + if (visible) mGraphics.drawImage(image, null, offset.x, offset.y); + } + + public void write(OutputStream stream) { + mLayersMasksInfo.setLayersInfo(mLayersInfo); + + DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream)); + try { + mHeader.write(out); + out.flush(); + + mColorMode.write(out); + mImageResources.write(out); + mLayersMasksInfo.write(out); + mLayersInfo.write(out); + out.flush(); + + mLayersInfo.writeImageData(out); + out.flush(); + + writeImage(mMergedImage, out, false); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static void writeImage(BufferedImage image, DataOutputStream out, boolean split) + throws IOException { + + if (!split) out.writeShort(0); + + int width = image.getWidth(); + int height = image.getHeight(); + + final int length = width * height; + int[] pixels = new int[length]; + + image.getData().getDataElements(0, 0, width, height, pixels); + + byte[] a = new byte[length]; + byte[] r = new byte[length]; + byte[] g = new byte[length]; + byte[] b = new byte[length]; + + for (int i = 0; i < length; i++) { + final int pixel = pixels[i]; + a[i] = (byte) ((pixel >> 24) & 0xFF); + r[i] = (byte) ((pixel >> 16) & 0xFF); + g[i] = (byte) ((pixel >> 8) & 0xFF); + b[i] = (byte) (pixel & 0xFF); + } + + if (split) out.writeShort(0); + if (split) out.write(a); + if (split) out.writeShort(0); + out.write(r); + if (split) out.writeShort(0); + out.write(g); + if (split) out.writeShort(0); + out.write(b); + if (!split) out.write(a); + } + + @SuppressWarnings({"UnusedDeclaration"}) + static class Header { + static final short MODE_BITMAP = 0; + static final short MODE_GRAYSCALE = 1; + static final short MODE_INDEXED = 2; + static final short MODE_RGB = 3; + static final short MODE_CMYK = 4; + static final short MODE_MULTI_CHANNEL = 7; + static final short MODE_DUOTONE = 8; + static final short MODE_LAB = 9; + + final byte[] mSignature = "8BPS".getBytes(); + final short mVersion = 1; + final byte[] mReserved = new byte[6]; + final short mChannelCount = 4; + final int mHeight; + final int mWidth; + final short mDepth = 8; + final short mMode = MODE_RGB; + + Header(int width, int height) { + mWidth = width; + mHeight = height; + } + + void write(DataOutputStream out) throws IOException { + out.write(mSignature); + out.writeShort(mVersion); + out.write(mReserved); + out.writeShort(mChannelCount); + out.writeInt(mHeight); + out.writeInt(mWidth); + out.writeShort(mDepth); + out.writeShort(mMode); + } + } + + // Unused at the moment + @SuppressWarnings({"UnusedDeclaration"}) + static class ColorMode { + final int mLength = 0; + + void write(DataOutputStream out) throws IOException { + out.writeInt(mLength); + } + } + + // Unused at the moment + @SuppressWarnings({"UnusedDeclaration"}) + static class ImageResources { + static final short RESOURCE_RESOLUTION_INFO = 0x03ED; + + int mLength = 0; + + final byte[] mSignature = "8BIM".getBytes(); + final short mResourceId = RESOURCE_RESOLUTION_INFO; + + final short mPad = 0; + + final int mDataLength = 16; + + final short mHorizontalDisplayUnit = 0x48; // 72 dpi + final int mHorizontalResolution = 1; + final short mWidthDisplayUnit = 1; + + final short mVerticalDisplayUnit = 0x48; // 72 dpi + final int mVerticalResolution = 1; + final short mHeightDisplayUnit = 1; + + ImageResources() { + mLength = mSignature.length; + mLength += 2; + mLength += 2; + mLength += 4; + mLength += 8; + mLength += 8; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mLength); + out.write(mSignature); + out.writeShort(mResourceId); + out.writeShort(mPad); + out.writeInt(mDataLength); + out.writeShort(mHorizontalDisplayUnit); + out.writeInt(mHorizontalResolution); + out.writeShort(mWidthDisplayUnit); + out.writeShort(mVerticalDisplayUnit); + out.writeInt(mVerticalResolution); + out.writeShort(mHeightDisplayUnit); + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + static class LayersMasksInfo { + int mMiscLength; + int mLayerInfoLength; + + void setLayersInfo(LayersInfo layersInfo) { + mLayerInfoLength = layersInfo.getLength(); + // Round to the next multiple of 2 + if ((mLayerInfoLength & 0x1) == 0x1) mLayerInfoLength++; + mMiscLength = mLayerInfoLength + 8; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mMiscLength); + out.writeInt(mLayerInfoLength); + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + static class LayersInfo { + final List<Layer> mLayers = new ArrayList<Layer>(); + + void addLayer(String name, BufferedImage image, Point offset, boolean visible) { + mLayers.add(new Layer(name, image, offset, visible)); + } + + int getLength() { + int length = 2; + for (Layer layer : mLayers) { + length += layer.getLength(); + } + return length; + } + + void write(DataOutputStream out) throws IOException { + out.writeShort((short) -mLayers.size()); + for (Layer layer : mLayers) { + layer.write(out); + } + } + + void writeImageData(DataOutputStream out) throws IOException { + for (Layer layer : mLayers) { + layer.writeImageData(out); + } + // Global layer mask info length + out.writeInt(0); + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + static class Layer { + static final byte OPACITY_TRANSPARENT = 0x0; + static final byte OPACITY_OPAQUE = (byte) 0xFF; + + static final byte CLIPPING_BASE = 0x0; + static final byte CLIPPING_NON_BASE = 0x1; + + static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1; + static final byte FLAG_INVISIBLE = 0x2; + + final int mTop; + final int mLeft; + final int mBottom; + final int mRight; + + final short mChannelCount = 4; + final Channel[] mChannelInfo = new Channel[mChannelCount]; + + final byte[] mBlendSignature = "8BIM".getBytes(); + final byte[] mBlendMode = "norm".getBytes(); + + final byte mOpacity = OPACITY_OPAQUE; + final byte mClipping = CLIPPING_BASE; + byte mFlags = 0x0; + final byte mFiller = 0x0; + + int mExtraSize = 4 + 4; + + final int mMaskDataLength = 0; + final int mBlendRangeDataLength = 0; + + final byte[] mName; + + final byte[] mLayerExtraSignature = "8BIM".getBytes(); + final byte[] mLayerExtraKey = "luni".getBytes(); + int mLayerExtraLength; + final String mOriginalName; + + private BufferedImage mImage; + + Layer(String name, BufferedImage image, Point offset, boolean visible) { + final int height = image.getHeight(); + final int width = image.getWidth(); + final int length = width * height; + + mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length); + mChannelInfo[1] = new Channel(Channel.ID_RED, length); + mChannelInfo[2] = new Channel(Channel.ID_GREEN, length); + mChannelInfo[3] = new Channel(Channel.ID_BLUE, length); + + mTop = offset.y; + mLeft = offset.x; + mBottom = offset.y + height; + mRight = offset.x + width; + + mOriginalName = name; + byte[] data = name.getBytes(); + + try { + mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length; + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + final byte[] nameData = new byte[data.length + 1]; + nameData[0] = (byte) (data.length & 0xFF); + System.arraycopy(data, 0, nameData, 1, data.length); + + // This could be done in the same pass as above + if (nameData.length % 4 != 0) { + data = new byte[nameData.length + 4 - (nameData.length % 4)]; + System.arraycopy(nameData, 0, data, 0, nameData.length); + mName = data; + } else { + mName = nameData; + } + mExtraSize += mName.length; + mExtraSize += mLayerExtraLength + 4 + mLayerExtraKey.length + + mLayerExtraSignature.length; + + mImage = image; + + if (!visible) { + mFlags |= FLAG_INVISIBLE; + } + } + + int getLength() { + int length = 4 * 4 + 2; + + for (Channel channel : mChannelInfo) { + length += channel.getLength(); + } + + length += mBlendSignature.length; + length += mBlendMode.length; + length += 4; + length += 4; + length += mExtraSize; + + return length; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mTop); + out.writeInt(mLeft); + out.writeInt(mBottom); + out.writeInt(mRight); + + out.writeShort(mChannelCount); + for (Channel channel : mChannelInfo) { + channel.write(out); + } + + out.write(mBlendSignature); + out.write(mBlendMode); + + out.write(mOpacity); + out.write(mClipping); + out.write(mFlags); + out.write(mFiller); + + out.writeInt(mExtraSize); + out.writeInt(mMaskDataLength); + + out.writeInt(mBlendRangeDataLength); + + out.write(mName); + + out.write(mLayerExtraSignature); + out.write(mLayerExtraKey); + out.writeInt(mLayerExtraLength); + out.writeInt(mOriginalName.length() + 1); + out.write(mOriginalName.getBytes("UTF-16")); + } + + void writeImageData(DataOutputStream out) throws IOException { + writeImage(mImage, out, true); + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + static class Channel { + static final short ID_RED = 0; + static final short ID_GREEN = 1; + static final short ID_BLUE = 2; + static final short ID_ALPHA = -1; + static final short ID_LAYER_MASK = -2; + + final short mId; + final int mDataLength; + + Channel(short id, int dataLength) { + mId = id; + mDataLength = dataLength + 2; + } + + int getLength() { + return 2 + 4 + mDataLength; + } + + void write(DataOutputStream out) throws IOException { + out.writeShort(mId); + out.writeInt(mDataLength); + } + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PsdFileFilter.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PsdFileFilter.java new file mode 100644 index 000000000..6a7ce5b93 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PsdFileFilter.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.ui.util; + +import javax.swing.filechooser.FileFilter; +import java.io.File; + +public class PsdFileFilter extends FileFilter { + @Override + public boolean accept(File f) { + return f.isDirectory() || f.getName().toLowerCase().endsWith(".psd"); + } + + @Override + public String getDescription() { + return "Photoshop Document (*.psd)"; + } +}
\ No newline at end of file diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/util/OS.java b/hierarchyviewer/src/com/android/hierarchyviewer/util/OS.java new file mode 100644 index 000000000..fd619d6df --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/util/OS.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.util; + +public class OS { + private static boolean macOs; + private static boolean leopard; + private static boolean linux; + private static boolean windows; + + static { + String osName = System.getProperty("os.name"); + macOs = "Mac OS X".startsWith(osName); + linux = "Linux".startsWith(osName); + windows = "Windows".startsWith(osName); + + String version = System.getProperty("os.version"); + final String[] parts = version.split("\\."); + leopard = Integer.parseInt(parts[0]) >= 10 && Integer.parseInt(parts[1]) >= 5; + } + + public static boolean isMacOsX() { + return macOs; + } + + public static boolean isLeopardOrLater() { + return leopard; + } + + public static boolean isLinux() { + return linux; + } + + public static boolean isWindows() { + return windows; + } +} diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/util/WorkerThread.java b/hierarchyviewer/src/com/android/hierarchyviewer/util/WorkerThread.java new file mode 100644 index 000000000..c714c1c31 --- /dev/null +++ b/hierarchyviewer/src/com/android/hierarchyviewer/util/WorkerThread.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 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.android.hierarchyviewer.util; + +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Simple utility class used only to mark methods that are not executed on the UI thread + * (or Event Dispatch Thread in Swing/AWT.) This annotation's sole purpose is to help + * reading the source code. It has no additional effect. + */ +@Target({ ElementType.METHOD }) +@Retention(RetentionPolicy.SOURCE) +public @interface WorkerThread { +} |