aboutsummaryrefslogtreecommitdiffstats
path: root/hierarchyviewer
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit1506a206c0a5e3b593c4c61a62b8805b64e98daf (patch)
treee20fe3eb0f693e87649fff1ce75e3f23330f69f8 /hierarchyviewer
downloadsdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.zip
sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.tar.gz
sdk-1506a206c0a5e3b593c4c61a62b8805b64e98daf.tar.bz2
Initial Contribution
Diffstat (limited to 'hierarchyviewer')
-rw-r--r--hierarchyviewer/Android.mk17
-rw-r--r--hierarchyviewer/MODULE_LICENSE_APACHE20
-rw-r--r--hierarchyviewer/etc/Android.mk20
-rwxr-xr-xhierarchyviewer/etc/hierarchyviewer63
-rwxr-xr-xhierarchyviewer/etc/hierarchyviewer.bat41
-rw-r--r--hierarchyviewer/etc/manifest.txt2
-rw-r--r--hierarchyviewer/src/Android.mk30
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java67
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/device/Configuration.java27
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/device/DeviceBridge.java190
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/device/Window.java45
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/laf/UnifiedContentBorder.java43
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/scene/CaptureLoader.java72
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java173
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java219
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java68
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.java167
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/scene/WindowsLoader.java87
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/CaptureRenderer.java86
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java121
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java732
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java1382
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/BackgroundAction.java29
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureNodeAction.java42
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ExitAction.java48
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/InvalidateAction.java42
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/LoadGraphAction.java43
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RefreshWindowsAction.java40
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/RequestLayoutAction.java42
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/SaveSceneAction.java43
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ShowDevicesAction.java44
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StartServerAction.java40
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/action/StopServerAction.java40
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/model/PropertiesTableModel.java92
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/model/ViewsTreeModel.java62
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/util/IconLoader.java49
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/ui/util/PngFileFilter.java32
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/util/OS.java51
-rw-r--r--hierarchyviewer/src/com/android/hierarchyviewer/util/WorkerThread.java32
-rw-r--r--hierarchyviewer/src/resources/images/icon-graph-view-selected.pngbin0 -> 749 bytes
-rw-r--r--hierarchyviewer/src/resources/images/icon-graph-view.pngbin0 -> 747 bytes
-rw-r--r--hierarchyviewer/src/resources/images/icon-pixel-perfect-view-selected.pngbin0 -> 734 bytes
-rw-r--r--hierarchyviewer/src/resources/images/icon-pixel-perfect-view.pngbin0 -> 733 bytes
43 files changed, 4423 insertions, 0 deletions
diff --git a/hierarchyviewer/Android.mk b/hierarchyviewer/Android.mk
new file mode 100644
index 0000000..110e2ed
--- /dev/null
+++ b/hierarchyviewer/Android.mk
@@ -0,0 +1,17 @@
+# 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.
+
+HIERARCHYVIEWER_LOCAL_DIR := $(call my-dir)
+include $(HIERARCHYVIEWER_LOCAL_DIR)/etc/Android.mk
+include $(HIERARCHYVIEWER_LOCAL_DIR)/src/Android.mk
diff --git a/hierarchyviewer/MODULE_LICENSE_APACHE2 b/hierarchyviewer/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/hierarchyviewer/MODULE_LICENSE_APACHE2
diff --git a/hierarchyviewer/etc/Android.mk b/hierarchyviewer/etc/Android.mk
new file mode 100644
index 0000000..2794a7f
--- /dev/null
+++ b/hierarchyviewer/etc/Android.mk
@@ -0,0 +1,20 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PREBUILT_EXECUTABLES := hierarchyviewer
+include $(BUILD_HOST_PREBUILT)
+
diff --git a/hierarchyviewer/etc/hierarchyviewer b/hierarchyviewer/etc/hierarchyviewer
new file mode 100755
index 0000000..4244434
--- /dev/null
+++ b/hierarchyviewer/etc/hierarchyviewer
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright 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.
+
+# Set up prog to be the path of this script, including following symlinks,
+# and set up progdir to be the fully-qualified pathname of its directory.
+prog="$0"
+while [ -h "${prog}" ]; do
+ newProg=`/bin/ls -ld "${prog}"`
+ newProg=`expr "${newProg}" : ".* -> \(.*\)$"`
+ if expr "x${newProg}" : 'x/' >/dev/null; then
+ prog="${newProg}"
+ else
+ progdir=`dirname "${prog}"`
+ prog="${progdir}/${newProg}"
+ fi
+done
+oldwd=`pwd`
+progdir=`dirname "${prog}"`
+cd "${progdir}"
+progdir=`pwd`
+prog="${progdir}"/`basename "${prog}"`
+cd "${oldwd}"
+
+jarfile=hierarchyviewer.jar
+frameworkdir="$progdir"
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/tools/lib
+ libdir=`dirname "$progdir"`/tools/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ frameworkdir=`dirname "$progdir"`/framework
+ libdir=`dirname "$progdir"`/lib
+fi
+if [ ! -r "$frameworkdir/$jarfile" ]
+then
+ echo `basename "$prog"`": can't find $jarfile"
+ exit 1
+fi
+
+if [ "$OSTYPE" = "cygwin" ] ; then
+ jarpath=`cygpath -w "$frameworkdir/$jarfile"`
+ progdir=`cygpath -w "$progdir"`
+else
+ jarpath="$frameworkdir/$jarfile"
+fi
+
+# need to use "java.ext.dirs" because "-jar" causes classpath to be ignored
+# might need more memory, e.g. -Xmx128M
+exec java -Xmx512M -Djava.ext.dirs="$frameworkdir" -Dhierarchyviewer.adb="$progdir" -jar "$jarpath" "$@"
diff --git a/hierarchyviewer/etc/hierarchyviewer.bat b/hierarchyviewer/etc/hierarchyviewer.bat
new file mode 100755
index 0000000..67e4f80
--- /dev/null
+++ b/hierarchyviewer/etc/hierarchyviewer.bat
@@ -0,0 +1,41 @@
+@echo off
+rem Copyright (C) 2008 The Android Open Source Project
+rem
+rem Licensed under the Apache License, Version 2.0 (the "License");
+rem you may not use this file except in compliance with the License.
+rem You may obtain a copy of the License at
+rem
+rem http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+
+rem don't modify the caller's environment
+setlocal
+
+rem Set up prog to be the path of this script, including following symlinks,
+rem and set up progdir to be the fully-qualified pathname of its directory.
+set prog=%~f0
+
+rem Change current directory to where ddms is, to avoid issues with directories
+rem containing whitespaces.
+cd %~dp0
+
+set jarfile=hierarchyviewer.jar
+set frameworkdir=
+set libdir=
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+ set frameworkdir=lib\
+
+if exist %frameworkdir%%jarfile% goto JarFileOk
+ set frameworkdir=..\framework\
+
+:JarFileOk
+
+set jarpath=%frameworkdir%%jarfile%
+
+call java -Xmx512m -Djava.ext.dirs=%frameworkdir% -Dhierarchyviewer.adb= -jar %jarpath% %*
diff --git a/hierarchyviewer/etc/manifest.txt b/hierarchyviewer/etc/manifest.txt
new file mode 100644
index 0000000..f7ddfa9
--- /dev/null
+++ b/hierarchyviewer/etc/manifest.txt
@@ -0,0 +1,2 @@
+Main-Class: com.android.hierarchyviewer.HierarchyViewer
+Class-Path: ddmlib.jar swing-worker-1.1.jar org-openide-util.jar org-netbeans-api-visual.jar
diff --git a/hierarchyviewer/src/Android.mk b/hierarchyviewer/src/Android.mk
new file mode 100644
index 0000000..0bc1f1e
--- /dev/null
+++ b/hierarchyviewer/src/Android.mk
@@ -0,0 +1,30 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_RESOURCE_DIRS := resources
+
+LOCAL_JAR_MANIFEST := ../etc/manifest.txt
+LOCAL_JAVA_LIBRARIES := \
+ ddmlib \
+ swing-worker-1.1 \
+ org-openide-util \
+ org-netbeans-api-visual
+LOCAL_MODULE := hierarchyviewer
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
diff --git a/hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java b/hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java
new file mode 100644
index 0000000..59ce67f
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/HierarchyViewer.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hierarchyviewer.ui.Workspace;
+import com.android.hierarchyviewer.device.DeviceBridge;
+
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.UnsupportedLookAndFeelException;
+
+public class HierarchyViewer {
+ private static final CharSequence OS_WINDOWS = "Windows";
+ private static final CharSequence OS_MACOSX = "Mac OS X";
+
+ 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();
+ }
+ }
+
+ public static void main(String[] args) {
+ initUserInterface();
+ DeviceBridge.initDebugBridge();
+
+ 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 0000000..090730f
--- /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 0000000..850a238
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/device/DeviceBridge.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.device;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+
+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<Device, Integer> devicePortMap = new HashMap<Device, 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 void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) {
+ AndroidDebugBridge.removeDeviceChangeListener(listener);
+ }
+
+ public static Device[] getDevices() {
+ return bridge.getDevices();
+ }
+
+ public static boolean isViewServerRunning(Device device) {
+ initDebugBridge();
+ final boolean[] result = new boolean[1];
+ try {
+ if (device.isOnline()) {
+ device.executeShellCommand(buildIsServerRunningShellCommand(),
+ new BooleanResultReader(result));
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return result[0];
+ }
+
+ public static boolean startViewServer(Device device) {
+ return startViewServer(device, Configuration.DEFAULT_SERVER_PORT);
+ }
+
+ public static boolean startViewServer(Device 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();
+ }
+ return result[0];
+ }
+
+ public static boolean stopViewServer(Device device) {
+ initDebugBridge();
+ final boolean[] result = new boolean[1];
+ try {
+ if (device.isOnline()) {
+ device.executeShellCommand(buildStopServerShellCommand(),
+ new BooleanResultReader(result));
+ }
+ } catch (IOException 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(Device device) {
+ synchronized (devicePortMap) {
+ if (device.getState() == Device.DeviceState.ONLINE) {
+ int localPort = nextLocalPort++;
+ device.createForward(localPort, Configuration.DEFAULT_SERVER_PORT);
+ devicePortMap.put(device, localPort);
+ }
+ }
+ }
+
+ public static void removeDeviceForward(Device device) {
+ synchronized (devicePortMap) {
+ final Integer localPort = devicePortMap.get(device);
+ if (localPort != null) {
+ device.removeForward(localPort, Configuration.DEFAULT_SERVER_PORT);
+ devicePortMap.remove(device);
+ }
+ }
+ }
+
+ public static int getDeviceLocalPort(Device 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 0000000..0417df6
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/device/Window.java
@@ -0,0 +1,45 @@
+/*
+ * 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);
+ }
+
+ 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 0000000..401fb3e
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/laf/UnifiedContentBorder.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.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);
+
+ 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);
+ }
+
+ public Insets getBorderInsets(Component component) {
+ return new Insets(1, 0, 2, 0);
+ }
+
+ 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 0000000..7cc44bc
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/CaptureLoader.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Device;
+import com.android.hierarchyviewer.device.Configuration;
+import com.android.hierarchyviewer.device.Window;
+import com.android.hierarchyviewer.device.DeviceBridge;
+
+import java.awt.Image;
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+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 Image loadCapture(Device 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/ViewHierarchyLoader.java b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java
new file mode 100644
index 0000000..6efb52d6
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyLoader.java
@@ -0,0 +1,173 @@
+/*
+ * 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.Device;
+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(Device 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()));
+
+ 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);
+ }
+ }
+
+ } 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 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 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 0000000..d99a80c
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewHierarchyScene.java
@@ -0,0 +1,219 @@
+/*
+ * 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.name, node.id);
+ widget.getActions().addAction(createSelectAction());
+ widget.getActions().addAction(moveAction);
+ widgetLayer.addChild(widget);
+ return widget;
+ }
+
+ private Widget createBox(String node, String id) {
+ Widget box = new GradientWidget(this);
+ 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(getShortName(node));
+ 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(node));
+ label.setBorder(BorderFactory.createEmptyBorder(3, 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(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 {
+ 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));
+
+ private static Color UNSELECTED = Color.BLACK;
+ private static Color SELECTED = Color.WHITE;
+
+ private boolean isSelected = false;
+ private GradientPaint gradient = MAC_OSX_SELECTED;
+
+ public GradientWidget(ViewHierarchyScene scene) {
+ super(scene);
+ }
+
+ @Override
+ protected void notifyStateChanged(ObjectState previous, ObjectState state) {
+ super.notifyStateChanged(previous, state);
+ isSelected = state.isSelected() || state.isFocused() || state.isWidgetFocused();
+
+ for (Widget child : getChildren()) {
+ child.setForeground(isSelected ? SELECTED : UNSELECTED);
+ }
+
+ repaint();
+ }
+
+ @Override
+ protected void paintBackground() {
+ super.paintBackground();
+
+ Graphics2D g2 = getGraphics();
+ Rectangle bounds = getBounds();
+
+ if (!isSelected) {
+ g2.setColor(Color.WHITE);
+ } else {
+ g2.setPaint(new GradientPaint(bounds.x, bounds.y, gradient.getColor1(),
+ bounds.x, bounds.x + bounds.height, gradient.getColor2()));
+ }
+ g2.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
+ }
+ }
+}
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 0000000..6b212c0
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewManager.java
@@ -0,0 +1,68 @@
+/*
+ * 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.Device;
+import com.android.hierarchyviewer.device.Configuration;
+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(Device device, Window window, String params) {
+ sendCommand("INVALIDATE", device, window, params);
+ }
+
+ public static void requestLayout(Device device, Window window, String params) {
+ sendCommand("REQUEST_LAYOUT", device, window, params);
+ }
+
+ private static void sendCommand(String command, Device 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 0000000..8284df1
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/ViewNode.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 java.awt.Image;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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;
+
+ public boolean decoded;
+
+ 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);
+
+ 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;
+ }
+
+ @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;
+ }
+ }
+}
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 0000000..6c14cb6
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/scene/WindowsLoader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.Device;
+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(Device device) {
+ Socket socket = null;
+ BufferedReader in = null;
+ BufferedWriter out = null;
+
+ 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) {
+ Window w = new Window(line.substring(index + 1),
+ Integer.parseInt(line.substring(0, index), 16));
+ 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 0000000..7ccc818
--- /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 0000000..a50905c
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/LayoutRenderer.java
@@ -0,0 +1,121 @@
+/*
+ * 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.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;
+
+ LayoutRenderer(ViewHierarchyScene scene) {
+ this.scene = scene;
+
+ setOpaque(true);
+ setBorder(BorderFactory.createEmptyBorder(0, 0, 12, 0));
+ setBackground(Color.BLACK);
+ setForeground(Color.WHITE);
+ }
+
+ @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();
+ }
+}
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 0000000..83d926f
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/ScreenViewer.java
@@ -0,0 +1,732 @@
+package com.android.hierarchyviewer.ui;
+
+import com.android.ddmlib.Device;
+import com.android.ddmlib.RawImage;
+import com.android.hierarchyviewer.util.WorkerThread;
+import com.android.hierarchyviewer.scene.ViewNode;
+import com.android.hierarchyviewer.ui.util.PngFileFilter;
+import com.android.hierarchyviewer.ui.util.IconLoader;
+
+import javax.swing.JComponent;
+import javax.swing.Timer;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JSlider;
+import javax.swing.Box;
+import javax.swing.JCheckBox;
+import javax.swing.JButton;
+import javax.swing.JFileChooser;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ChangeEvent;
+import javax.imageio.ImageIO;
+
+import org.jdesktop.swingworker.SwingWorker;
+
+import java.io.IOException;
+import java.io.File;
+import java.awt.image.BufferedImage;
+import java.awt.Graphics;
+import java.awt.Dimension;
+import java.awt.BorderLayout;
+import java.awt.Graphics2D;
+import java.awt.Color;
+import java.awt.Rectangle;
+import java.awt.Point;
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.FlowLayout;
+import java.awt.AlphaComposite;
+import java.awt.RenderingHints;
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseMotionAdapter;
+import java.util.concurrent.ExecutionException;
+
+class ScreenViewer extends JPanel implements ActionListener {
+ private final Workspace workspace;
+ private final Device 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;
+
+ ScreenViewer(Workspace workspace, Device device, int spacing) {
+ setLayout(new BorderLayout());
+ setOpaque(false);
+
+ this.workspace = workspace;
+ this.device = device;
+
+ timer = new Timer(5000, this);
+ timer.setInitialDelay(0);
+ timer.setRepeats(true);
+
+ JPanel panel = buildViewerAndControls();
+ add(panel, BorderLayout.WEST);
+
+ JPanel loupePanel = buildLoupePanel(spacing);
+ add(loupePanel, BorderLayout.CENTER);
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ timer.start();
+ }
+ });
+ }
+
+ private JPanel buildLoupePanel(int spacing) {
+ loupe = new LoupeViewer();
+ 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 JPanel buildViewerAndControls() {
+ JPanel panel = new JPanel(new GridBagLayout());
+ crosshair = new Crosshair(new ScreenshotViewer());
+ panel.add(crosshair,
+ new GridBagConstraints(0, y++, 2, 1, 1.0f, 0.0f,
+ GridBagConstraints.FIRST_LINE_START, GridBagConstraints.NONE,
+ 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();
+ }
+ });
+ buildSlider(panel, "Zoom:", "2x", "24x", 2, 24, 8, 2).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;
+
+ Crosshair(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 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 && rawImage.bpp == 16) {
+ 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;
+ }
+
+ 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);
+ }
+ }
+ } finally {
+ isLoading = false;
+ }
+
+ return resize;
+ }
+
+ @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 0000000..0add4e9
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/Workspace.java
@@ -0,0 +1,1382 @@
+/*
+ * 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.Device;
+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.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.util.OS;
+import com.android.hierarchyviewer.util.WorkerThread;
+import com.android.hierarchyviewer.ui.action.ShowDevicesAction;
+import com.android.hierarchyviewer.ui.action.RequestLayoutAction;
+import com.android.hierarchyviewer.ui.action.InvalidateAction;
+import com.android.hierarchyviewer.ui.action.CaptureNodeAction;
+import com.android.hierarchyviewer.ui.action.RefreshWindowsAction;
+import com.android.hierarchyviewer.ui.action.StopServerAction;
+import com.android.hierarchyviewer.ui.action.StartServerAction;
+import com.android.hierarchyviewer.ui.action.ExitAction;
+import com.android.hierarchyviewer.ui.action.LoadGraphAction;
+import com.android.hierarchyviewer.ui.action.SaveSceneAction;
+import com.android.hierarchyviewer.ui.util.PngFileFilter;
+import com.android.hierarchyviewer.ui.util.IconLoader;
+import com.android.hierarchyviewer.ui.model.PropertiesTableModel;
+import com.android.hierarchyviewer.ui.model.ViewsTreeModel;
+import org.jdesktop.swingworker.SwingWorker;
+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 javax.imageio.ImageIO;
+import javax.swing.ActionMap;
+import javax.swing.BorderFactory;
+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.JScrollPane;
+import javax.swing.JSlider;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.JToolBar;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
+import javax.swing.JTree;
+import javax.swing.Box;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.DefaultTreeCellRenderer;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.table.DefaultTableModel;
+import java.awt.image.BufferedImage;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import java.awt.FlowLayout;
+import java.awt.Color;
+import java.awt.Image;
+import java.awt.Graphics2D;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+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;
+
+public class Workspace extends JFrame {
+ private JLabel viewCountLabel;
+ private JSlider zoomSlider;
+ private JSplitPane sideSplitter;
+ private JSplitPane mainSplitter;
+ private JTable propertiesTable;
+ 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 JComponent deviceSelector;
+ private DevicesTableModel devicesTableModel;
+ private WindowsTableModel windowsTableModel;
+
+ private Device currentDevice;
+ private Window currentWindow = Window.FOCUSED_WINDOW;
+
+ private JButton displayNodeButton;
+ 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;
+
+ public Workspace() {
+ super("Hierarchy Viewer");
+
+ buildActions();
+ add(buildMainPanel());
+ setJMenuBar(buildMenuBar());
+
+ 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(CaptureNodeAction.ACTION_NAME, new CaptureNodeAction(this));
+ actionsMap.put(RefreshWindowsAction.ACTION_NAME, new RefreshWindowsAction(this));
+ }
+
+ private JComponent buildMainPanel() {
+ mainPanel = new JPanel();
+ mainPanel.setLayout(new BorderLayout());
+ mainPanel.add(buildToolBar(), BorderLayout.PAGE_START);
+ mainPanel.add(deviceSelector = buildDeviceSelector(), BorderLayout.CENTER);
+ mainPanel.add(buildStatusPanel(), BorderLayout.SOUTH);
+
+ mainPanel.setPreferredSize(new Dimension(950, 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 (Device 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);
+
+ sideSplitter = new JSplitPane();
+ sideSplitter.setBorder(null);
+ sideSplitter.setOrientation(JSplitPane.VERTICAL_SPLIT);
+ sideSplitter.setResizeWeight(0.5);
+ sideSplitter.setLeftComponent(tableScroller);
+ 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);
+
+ minZoomLabel = new JLabel();
+ minZoomLabel.setText("20%");
+ minZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
+ minZoomLabel.setBorder(BorderFactory.createEmptyBorder(0, 6, 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);
+
+ viewCountLabel.setVisible(false);
+ zoomSlider.setVisible(false);
+ minZoomLabel.setVisible(false);
+ maxZoomLabel.setVisible(false);
+
+ return statusPanel;
+ }
+
+ 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);
+
+ 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 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() {
+ viewCountLabel.setVisible(true);
+ zoomSlider.setVisible(true);
+ minZoomLabel.setVisible(true);
+ maxZoomLabel.setVisible(true);
+
+ screenViewer.stop();
+ mainPanel.remove(pixelPerfectPanel);
+ mainPanel.add(mainSplitter, BorderLayout.CENTER);
+
+ validate();
+ repaint();
+ }
+
+ private void togglePixelPerfectView() {
+ if (pixelPerfectPanel == null) {
+ pixelPerfectPanel = buildPixelPerfectPanel();
+ showPixelPerfectTree();
+ } else {
+ screenViewer.start();
+ }
+
+ viewCountLabel.setVisible(false);
+ zoomSlider.setVisible(false);
+ minZoomLabel.setVisible(false);
+ maxZoomLabel.setVisible(false);
+
+ 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 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);
+
+ viewCountLabel.setVisible(true);
+ zoomSlider.setVisible(true);
+ minZoomLabel.setVisible(true);
+ maxZoomLabel.setVisible(true);
+ }
+
+ sceneView = scene.createView();
+ sceneView.addMouseListener(new NodeClickListener());
+ sceneScroller.setViewportView(sceneView);
+
+ if (extrasPanel != null) {
+ sideSplitter.remove(extrasPanel);
+ }
+ sideSplitter.setBottomComponent(buildExtrasPanel());
+
+ mainSplitter.setDividerLocation(getWidth() - mainSplitter.getDividerSize() -
+ buttonsPanel.getPreferredSize().width);
+
+ 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());
+ extrasPanel.add(new JScrollPane(layoutView = new LayoutRenderer(scene)));
+ 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);
+
+ viewCountLabel.setVisible(false);
+ zoomSlider.setVisible(false);
+ minZoomLabel.setVisible(false);
+ maxZoomLabel.setVisible(false);
+
+ saveMenuItem.setEnabled(false);
+ showDevicesMenuItem.setEnabled(false);
+ showDevicesButton.setEnabled(false);
+ displayNodeButton.setEnabled(false);
+ invalidateButton.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);
+ invalidateButton.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 (Device 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);
+ }
+ }
+ });
+ }
+
+ 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<?, ?> 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 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 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();
+ }
+ }
+ }
+
+ private class LoadWindowsTask extends SwingWorker<Window[], Void> {
+ private LoadWindowsTask() {
+ beginTask();
+ }
+
+ @Override
+ @WorkerThread
+ protected Window[] doInBackground() throws Exception {
+ return WindowsLoader.loadWindows(currentDevice);
+ }
+
+ @Override
+ protected void done() {
+ try {
+ windowsTableModel.clear();
+ windowsTableModel.addWindows(get());
+ } 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<ViewHierarchyScene, Void> {
+ public LoadGraphTask() {
+ beginTask();
+ }
+
+ @Override
+ @WorkerThread
+ protected ViewHierarchyScene doInBackground() {
+ scene = ViewHierarchyLoader.loadScene(currentDevice, currentWindow);
+ return scene;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ createGraph(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);
+ 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 DevicesTableModel extends DefaultTableModel implements
+ AndroidDebugBridge.IDeviceChangeListener {
+
+ private ArrayList<Device> devices;
+
+ private DevicesTableModel() {
+ devices = new ArrayList<Device>();
+ }
+
+ @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 Device device) {
+ DeviceBridge.setupDeviceForward(device);
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ addDevice(device);
+ }
+ });
+ }
+
+ @WorkerThread
+ public void deviceDisconnected(final Device device) {
+ DeviceBridge.removeDeviceForward(device);
+
+ SwingUtilities.invokeLater(new Runnable() {
+ public void run() {
+ removeDevice(device);
+ }
+ });
+ }
+
+ public void addDevice(Device device) {
+ if (!devices.contains(device)) {
+ devices.add(device);
+ fireTableDataChanged();
+ }
+ }
+
+ public void removeDevice(Device device) {
+ if (device.equals(currentDevice)) {
+ reset();
+ }
+
+ if (devices.contains(device)) {
+ devices.remove(device);
+ fireTableDataChanged();
+ }
+ }
+
+ @WorkerThread
+ public void deviceChanged(Device device, int changeMask) {
+ if ((changeMask & Device.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 & Device.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 Device getDevice(int index) {
+ return index < devices.size() ? devices.get(index) : null;
+ }
+
+ public Device[] getDevices() {
+ return devices.toArray(new Device[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 0000000..051e3f3
--- /dev/null
+++ b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/BackgroundAction.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.jdesktop.swingworker.SwingWorker;
+
+import javax.swing.AbstractAction;
+
+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/CaptureNodeAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/CaptureNodeAction.java
new file mode 100644
index 0000000..a8aee3c
--- /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/ExitAction.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/action/ExitAction.java
new file mode 100644
index 0000000..e5aaed5
--- /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 0000000..7767cda
--- /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 0000000..42c8b8e
--- /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 0000000..bcb7ea7
--- /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 0000000..6fc2832
--- /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 0000000..7c7a8a9
--- /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 0000000..a91ab7a
--- /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 0000000..ccb6ae0
--- /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 0000000..ac76e14
--- /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/PropertiesTableModel.java b/hierarchyviewer/src/com/android/hierarchyviewer/ui/model/PropertiesTableModel.java
new file mode 100644
index 0000000..cc4f7e3
--- /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 0000000..f3a07f8
--- /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 0000000..ef73956
--- /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 0000000..5d6472d
--- /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/util/OS.java b/hierarchyviewer/src/com/android/hierarchyviewer/util/OS.java
new file mode 100644
index 0000000..fd619d6
--- /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 0000000..c714c1c
--- /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 {
+}
diff --git a/hierarchyviewer/src/resources/images/icon-graph-view-selected.png b/hierarchyviewer/src/resources/images/icon-graph-view-selected.png
new file mode 100644
index 0000000..91f7119
--- /dev/null
+++ b/hierarchyviewer/src/resources/images/icon-graph-view-selected.png
Binary files differ
diff --git a/hierarchyviewer/src/resources/images/icon-graph-view.png b/hierarchyviewer/src/resources/images/icon-graph-view.png
new file mode 100644
index 0000000..9a7f68b
--- /dev/null
+++ b/hierarchyviewer/src/resources/images/icon-graph-view.png
Binary files differ
diff --git a/hierarchyviewer/src/resources/images/icon-pixel-perfect-view-selected.png b/hierarchyviewer/src/resources/images/icon-pixel-perfect-view-selected.png
new file mode 100644
index 0000000..1e44000
--- /dev/null
+++ b/hierarchyviewer/src/resources/images/icon-pixel-perfect-view-selected.png
Binary files differ
diff --git a/hierarchyviewer/src/resources/images/icon-pixel-perfect-view.png b/hierarchyviewer/src/resources/images/icon-pixel-perfect-view.png
new file mode 100644
index 0000000..ec51cec
--- /dev/null
+++ b/hierarchyviewer/src/resources/images/icon-pixel-perfect-view.png
Binary files differ