1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.hierarchyviewerlib.ui; 18 19 import com.android.ddmuilib.ImageLoader; 20 import com.android.hierarchyviewerlib.models.TreeViewModel; 21 import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; 22 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; 23 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; 24 import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; 25 26 import org.eclipse.swt.SWT; 27 import org.eclipse.swt.events.DisposeEvent; 28 import org.eclipse.swt.events.DisposeListener; 29 import org.eclipse.swt.events.MouseEvent; 30 import org.eclipse.swt.events.MouseListener; 31 import org.eclipse.swt.events.MouseMoveListener; 32 import org.eclipse.swt.events.PaintEvent; 33 import org.eclipse.swt.events.PaintListener; 34 import org.eclipse.swt.graphics.GC; 35 import org.eclipse.swt.graphics.Image; 36 import org.eclipse.swt.graphics.Path; 37 import org.eclipse.swt.graphics.Transform; 38 import org.eclipse.swt.widgets.Canvas; 39 import org.eclipse.swt.widgets.Composite; 40 import org.eclipse.swt.widgets.Display; 41 import org.eclipse.swt.widgets.Event; 42 import org.eclipse.swt.widgets.Listener; 43 44 public class TreeViewOverview extends Canvas implements ITreeChangeListener { 45 46 private TreeViewModel mModel; 47 48 private DrawableViewNode mTree; 49 50 private Rectangle mViewport; 51 52 private Transform mTransform; 53 54 private Transform mInverse; 55 56 private Rectangle mBounds = new Rectangle(); 57 58 private double mScale; 59 60 private boolean mDragging = false; 61 62 private DrawableViewNode mSelectedNode; 63 64 private static Image sNotSelectedImage; 65 66 private static Image sSelectedImage; 67 68 private static Image sFilteredImage; 69 70 private static Image sFilteredSelectedImage; 71 72 public TreeViewOverview(Composite parent) { 73 super(parent, SWT.NONE); 74 75 mModel = TreeViewModel.getModel(); 76 mModel.addTreeChangeListener(this); 77 78 loadResources(); 79 80 addPaintListener(mPaintListener); 81 addMouseListener(mMouseListener); 82 addMouseMoveListener(mMouseMoveListener); 83 addListener(SWT.Resize, mResizeListener); 84 addDisposeListener(mDisposeListener); 85 86 mTransform = new Transform(Display.getDefault()); 87 mInverse = new Transform(Display.getDefault()); 88 89 loadAllData(); 90 } 91 92 private void loadResources() { 93 ImageLoader loader = ImageLoader.getLoader(this.getClass()); 94 sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ 95 sSelectedImage = loader.loadImage("selected-small.png", Display.getDefault()); //$NON-NLS-1$ 96 sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ 97 sFilteredSelectedImage = 98 loader.loadImage("selected-filtered-small.png", Display.getDefault()); //$NON-NLS-1$ 99 } 100 101 private DisposeListener mDisposeListener = new DisposeListener() { 102 @Override 103 public void widgetDisposed(DisposeEvent e) { 104 mModel.removeTreeChangeListener(TreeViewOverview.this); 105 mTransform.dispose(); 106 mInverse.dispose(); 107 } 108 }; 109 110 private MouseListener mMouseListener = new MouseListener() { 111 112 @Override 113 public void mouseDoubleClick(MouseEvent e) { 114 // pass 115 } 116 117 @Override 118 public void mouseDown(MouseEvent e) { 119 boolean redraw = false; 120 synchronized (TreeViewOverview.this) { 121 if (mTree != null && mViewport != null) { 122 mDragging = true; 123 redraw = true; 124 handleMouseEvent(transformPoint(e.x, e.y)); 125 } 126 } 127 if (redraw) { 128 mModel.removeTreeChangeListener(TreeViewOverview.this); 129 mModel.setViewport(mViewport); 130 mModel.addTreeChangeListener(TreeViewOverview.this); 131 doRedraw(); 132 } 133 } 134 135 @Override 136 public void mouseUp(MouseEvent e) { 137 boolean redraw = false; 138 synchronized (TreeViewOverview.this) { 139 if (mTree != null && mViewport != null) { 140 mDragging = false; 141 redraw = true; 142 handleMouseEvent(transformPoint(e.x, e.y)); 143 144 // Update bounds and transform only on mouse up. That way, 145 // you don't get confusing behaviour during mouse drag and 146 // it snaps neatly at the end 147 setBounds(); 148 setTransform(); 149 } 150 } 151 if (redraw) { 152 mModel.removeTreeChangeListener(TreeViewOverview.this); 153 mModel.setViewport(mViewport); 154 mModel.addTreeChangeListener(TreeViewOverview.this); 155 doRedraw(); 156 } 157 } 158 159 }; 160 161 private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { 162 @Override 163 public void mouseMove(MouseEvent e) { 164 boolean moved = false; 165 synchronized (TreeViewOverview.this) { 166 if (mDragging) { 167 moved = true; 168 handleMouseEvent(transformPoint(e.x, e.y)); 169 } 170 } 171 if (moved) { 172 mModel.removeTreeChangeListener(TreeViewOverview.this); 173 mModel.setViewport(mViewport); 174 mModel.addTreeChangeListener(TreeViewOverview.this); 175 doRedraw(); 176 } 177 } 178 }; 179 180 private void handleMouseEvent(Point pt) { 181 mViewport.x = pt.x - mViewport.width / 2; 182 mViewport.y = pt.y - mViewport.height / 2; 183 if (mViewport.x < mBounds.x) { 184 mViewport.x = mBounds.x; 185 } 186 if (mViewport.y < mBounds.y) { 187 mViewport.y = mBounds.y; 188 } 189 if (mViewport.x + mViewport.width > mBounds.x + mBounds.width) { 190 mViewport.x = mBounds.x + mBounds.width - mViewport.width; 191 } 192 if (mViewport.y + mViewport.height > mBounds.y + mBounds.height) { 193 mViewport.y = mBounds.y + mBounds.height - mViewport.height; 194 } 195 } 196 197 private Point transformPoint(double x, double y) { 198 float[] pt = { 199 (float) x, (float) y 200 }; 201 mInverse.transform(pt); 202 return new Point(pt[0], pt[1]); 203 } 204 205 private Listener mResizeListener = new Listener() { 206 @Override 207 public void handleEvent(Event arg0) { 208 synchronized (TreeViewOverview.this) { 209 setTransform(); 210 } 211 doRedraw(); 212 } 213 }; 214 215 private PaintListener mPaintListener = new PaintListener() { 216 @Override 217 public void paintControl(PaintEvent e) { 218 synchronized (TreeViewOverview.this) { 219 if (mTree != null) { 220 e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); 221 e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); 222 e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); 223 e.gc.setTransform(mTransform); 224 e.gc.setLineWidth((int) Math.ceil(0.7 / mScale)); 225 Path connectionPath = new Path(Display.getDefault()); 226 paintRecursive(e.gc, mTree, connectionPath); 227 e.gc.drawPath(connectionPath); 228 connectionPath.dispose(); 229 230 if (mViewport != null) { 231 e.gc.setAlpha(50); 232 e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); 233 e.gc.fillRectangle((int) mViewport.x, (int) mViewport.y, (int) Math 234 .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); 235 236 e.gc.setAlpha(255); 237 e.gc 238 .setForeground(Display.getDefault().getSystemColor( 239 SWT.COLOR_DARK_GRAY)); 240 e.gc.setLineWidth((int) Math.ceil(2 / mScale)); 241 e.gc.drawRectangle((int) mViewport.x, (int) mViewport.y, (int) Math 242 .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); 243 } 244 } 245 } 246 } 247 }; 248 249 private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) { 250 if (mSelectedNode == node && node.viewNode.filtered) { 251 gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); 252 } else if (mSelectedNode == node) { 253 gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); 254 } else if (node.viewNode.filtered) { 255 gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); 256 } else { 257 gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); 258 } 259 int N = node.children.size(); 260 if (N == 0) { 261 return; 262 } 263 float childSpacing = 264 (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N; 265 for (int i = 0; i < N; i++) { 266 DrawableViewNode child = node.children.get(i); 267 paintRecursive(gc, child, connectionPath); 268 float x1 = node.left + DrawableViewNode.NODE_WIDTH; 269 float y1 = 270 (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2; 271 float x2 = child.left; 272 float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; 273 float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; 274 float cy1 = y1; 275 float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; 276 float cy2 = y2; 277 connectionPath.moveTo(x1, y1); 278 connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); 279 } 280 } 281 282 private void doRedraw() { 283 Display.getDefault().syncExec(new Runnable() { 284 @Override 285 public void run() { 286 redraw(); 287 } 288 }); 289 } 290 291 public void loadAllData() { 292 Display.getDefault().syncExec(new Runnable() { 293 @Override 294 public void run() { 295 synchronized (this) { 296 mTree = mModel.getTree(); 297 mSelectedNode = mModel.getSelection(); 298 mViewport = mModel.getViewport(); 299 setBounds(); 300 setTransform(); 301 } 302 } 303 }); 304 } 305 306 // Note the syncExec and then synchronized... It avoids deadlock 307 @Override 308 public void treeChanged() { 309 Display.getDefault().syncExec(new Runnable() { 310 @Override 311 public void run() { 312 synchronized (this) { 313 mTree = mModel.getTree(); 314 mSelectedNode = mModel.getSelection(); 315 mViewport = mModel.getViewport(); 316 setBounds(); 317 setTransform(); 318 } 319 } 320 }); 321 doRedraw(); 322 } 323 324 private void setBounds() { 325 if (mViewport != null && mTree != null) { 326 mBounds.x = Math.min(mViewport.x, mTree.bounds.x); 327 mBounds.y = Math.min(mViewport.y, mTree.bounds.y); 328 mBounds.width = 329 Math.max(mViewport.x + mViewport.width, mTree.bounds.x + mTree.bounds.width) 330 - mBounds.x; 331 mBounds.height = 332 Math.max(mViewport.y + mViewport.height, mTree.bounds.y + mTree.bounds.height) 333 - mBounds.y; 334 } else if (mTree != null) { 335 mBounds.x = mTree.bounds.x; 336 mBounds.y = mTree.bounds.y; 337 mBounds.width = mTree.bounds.x + mTree.bounds.width - mBounds.x; 338 mBounds.height = mTree.bounds.y + mTree.bounds.height - mBounds.y; 339 } 340 } 341 342 private void setTransform() { 343 if (mTree != null) { 344 345 mTransform.identity(); 346 mInverse.identity(); 347 final Point size = new Point(); 348 size.x = getBounds().width; 349 size.y = getBounds().height; 350 if (mBounds.width == 0 || mBounds.height == 0 || size.x == 0 || size.y == 0) { 351 mScale = 1; 352 } else { 353 mScale = Math.min(size.x / mBounds.width, size.y / mBounds.height); 354 } 355 mTransform.scale((float) mScale, (float) mScale); 356 mInverse.scale((float) mScale, (float) mScale); 357 mTransform.translate((float) -mBounds.x, (float) -mBounds.y); 358 mInverse.translate((float) -mBounds.x, (float) -mBounds.y); 359 if (size.x / mBounds.width < size.y / mBounds.height) { 360 mTransform.translate(0, (float) (size.y / mScale - mBounds.height) / 2); 361 mInverse.translate(0, (float) (size.y / mScale - mBounds.height) / 2); 362 } else { 363 mTransform.translate((float) (size.x / mScale - mBounds.width) / 2, 0); 364 mInverse.translate((float) (size.x / mScale - mBounds.width) / 2, 0); 365 } 366 mInverse.invert(); 367 } 368 } 369 370 @Override 371 public void viewportChanged() { 372 Display.getDefault().syncExec(new Runnable() { 373 @Override 374 public void run() { 375 synchronized (this) { 376 mViewport = mModel.getViewport(); 377 setBounds(); 378 setTransform(); 379 } 380 } 381 }); 382 doRedraw(); 383 } 384 385 @Override 386 public void zoomChanged() { 387 viewportChanged(); 388 } 389 390 @Override 391 public void selectionChanged() { 392 synchronized (this) { 393 mSelectedNode = mModel.getSelection(); 394 } 395 doRedraw(); 396 } 397 } 398