View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package sk.baka.ambient.views.gesturelist;
20  
21  import java.util.List;
22  import java.util.concurrent.Callable;
23  import java.util.concurrent.CopyOnWriteArrayList;
24  
25  import sk.baka.ambient.AmbientApplication;
26  import sk.baka.ambient.R;
27  import sk.baka.ambient.collection.TrackMetadataBean;
28  import sk.baka.ambient.commons.Interval;
29  import sk.baka.ambient.commons.MiscUtils;
30  import sk.baka.ambient.views.ViewUtils;
31  import sk.baka.ambient.views.gesturelist.MouseGesturesRecognizer.GestureEnum;
32  import sk.baka.ambient.views.gesturelist.keypad.KeypadController;
33  import android.graphics.Point;
34  import android.os.Handler;
35  import android.view.Gravity;
36  import android.view.MotionEvent;
37  import android.view.View;
38  import android.view.ViewGroup.LayoutParams;
39  import android.widget.PopupWindow;
40  import android.widget.TextView;
41  
42  /***
43   * Controls the {@link GesturesListView} component via the touchpad.
44   * 
45   * @author Martin Vysny
46   */
47  final class TouchPadController {
48  	private final Handler handler = AmbientApplication.getHandler();
49  
50  	/***
51  	 * The view being controlled.
52  	 */
53  	private final GesturesListView owningView;
54  
55  	/***
56  	 * Creates new controller.
57  	 * 
58  	 * @param view
59  	 *            the view to control.
60  	 */
61  	TouchPadController(final GesturesListView view) {
62  		super();
63  		this.owningView = view;
64  	}
65  
66  	/***
67  	 * The gesture recognizer.
68  	 */
69  	private final MouseGesturesRecognizer gestureRecognizer = new MouseGesturesRecognizer();
70  
71  	/***
72  	 * If <code>false</code> then gesture handling is disabled until next
73  	 * PEN_DOWN event. Useful when leaving scrolling to the android component.
74  	 */
75  	private boolean enableGestureProcessing = true;
76  
77  	/***
78  	 * If <code>true</code> then we are selecting items with the R/RD gesture.
79  	 * Default event processing is disabled.
80  	 */
81  	private boolean highlighting = false;
82  
83  	/***
84  	 * If <code>true</code> then super {@link #onTouchEvent(MotionEvent)} is
85  	 * not invoked.
86  	 */
87  	private boolean suppressHandler = false;
88  
89  	/***
90  	 * Initial PEN_DOWN point.
91  	 */
92  	private Point initialPoint;
93  
94  	/***
95  	 * Initial index of the item.
96  	 */
97  	private int initialItem;
98  
99  	/***
100 	 * If <code>true</code> then the LU/LD gestures are in effect.
101 	 */
102 	private boolean draggingItems = false;
103 
104 	/***
105 	 * Reflects last {@link MotionEvent} coordinates.
106 	 */
107 	private final Point currentEventPoint = new Point();
108 
109 	/***
110 	 * If {@link #draggingItems dragging} then this window contain an overview
111 	 * of dragged items attached to the cursor.
112 	 */
113 	private PopupWindow dragHintWindow;
114 
115 	private boolean canHighlight() {
116 		if (owningView.listener.canHighlight()) {
117 			return true;
118 		}
119 		owningView.setTooltip(R.string.cannotSelect, this, false);
120 		initialItem = -1;
121 		draggingItems = false;
122 		highlighting = false;
123 		enableGestureProcessing = false;
124 		suppressHandler = false;
125 		return false;
126 	}
127 
128 	/***
129 	 * Handles the {@link View#onTouchEvent(MotionEvent)} events.
130 	 * 
131 	 * @param event
132 	 *            the event to handle
133 	 * @return if <code>false</code> then the event is not handled and should
134 	 *         be routed to super class.
135 	 */
136 	boolean onTouchEvent(MotionEvent event) {
137 		currentEventPoint.x = (int) event.getX();
138 		currentEventPoint.y = (int) event.getY();
139 		if (event.getAction() == MotionEvent.ACTION_DOWN) {
140 			enableGestureProcessing = true;
141 			suppressHandler = false;
142 			initialPoint = ViewUtils.clone(currentEventPoint);
143 			initialItem = owningView.pointToPosition(initialPoint.x,
144 					initialPoint.y);
145 			final boolean isEOP = owningView.isEOP(initialItem);
146 			if (isEOP) {
147 				initialItem = -1;
148 			}
149 		}
150 		if (enableGestureProcessing) {
151 			handleGestures(event);
152 		}
153 		if (highlighting) {
154 			// post highlight event
155 			int item = owningView.getItemIndex(event);
156 			if (item < 0) {
157 				if (event.getY() < 0) {
158 					item = 0;
159 				} else {
160 					item = owningView.getCount() - 1;
161 				}
162 			}
163 			if (!owningView.getHighlight().equals(initialItem, item)) {
164 				owningView.getModel().highlight(
165 						Interval.fromRange(initialItem, item));
166 				owningView.getModel().notifyModified();
167 			}
168 			if (ViewUtils.isPenUp(event)) {
169 				owningView.clearTooltip(this);
170 				highlighting = false;
171 				owningView.restoreSelector();
172 				handleScrolling(null, true);
173 			} else {
174 				handleScrolling(currentEventPoint, false);
175 			}
176 		}
177 		if (draggingItems) {
178 			utils.translateCoordinatesToRoot(currentEventPoint, owningView);
179 			if (dragHintWindow != null) {
180 				dragHintWindow.update(utils.translated.x, utils.translated.y,
181 						-1, -1);
182 			}
183 			final GesturesListView target = owningView
184 					.findView(currentEventPoint);
185 			if (ViewUtils.isPenUp(event)) {
186 				// PEN_UP. Look up a GesturesListView underneath the cursor
187 				// and drop the items there.
188 				owningView.clearTooltip(this);
189 				setDragging(false);
190 				if (dragHintWindow != null) {
191 					dragHintWindow.dismiss();
192 					dragHintWindow = null;
193 				}
194 				if (target != null) {
195 					if (!ViewUtils.isCancel(event)) {
196 						invokeDragDropEvent(currentEventPoint);
197 					}
198 					target.touchController.handleScrolling(null, true);
199 				}
200 			} else {
201 				if (target != null) {
202 					utils.translateCoordinates(currentEventPoint, owningView,
203 							target);
204 					target.touchController.handleScrolling(utils.translated,
205 							false);
206 				}
207 			}
208 		}
209 		return suppressHandler;
210 	}
211 
212 	/***
213 	 * Checks if this event activates a gesture, and in such case acts
214 	 * accordingly.
215 	 * 
216 	 * @param event
217 	 *            the mouse event.
218 	 */
219 	private void handleGestures(MotionEvent event) {
220 		final GestureEnum g = gestureRecognizer.processMouseEvent(event);
221 		final String gesture = gestureRecognizer.getGesture();
222 		if (g == GestureEnum.NewGesture) {
223 			final char firstGesture = gestureRecognizer.getLastGesture();
224 			// prevent handler to grab PEN_UP event which will result in
225 			// onClick event
226 			suppressHandler = true;
227 			if ((firstGesture == MouseGesturesRecognizer.DOWN_MOVE)
228 					|| (firstGesture == MouseGesturesRecognizer.UP_MOVE)) {
229 				// scrolling event. disable gesture processing and let
230 				// android scroll by itself
231 				enableGestureProcessing = false;
232 				suppressHandler = false;
233 			} else if (firstGesture == MouseGesturesRecognizer.RIGHT_MOVE) {
234 				// select a single item
235 				if (canHighlight()) {
236 					final int item = initialItem;
237 					if (item >= 0) {
238 						owningView.getModel()
239 								.highlight(Interval.fromItem(item));
240 						owningView.getModel().notifyModified();
241 					}
242 					owningView.setTooltip(R.string.selectionTouch, this, true);
243 				}
244 			} else {
245 				assert firstGesture == MouseGesturesRecognizer.LEFT_MOVE;
246 				owningView.setTooltip(owningView.hintDeleteCopyMoveId, this, true);
247 				// do nothing, wait for further gestures
248 			}
249 		} else if (g == GestureEnum.GestureFinished) {
250 			boolean clearMode = true;
251 			if ("L".equals(gesture)) {
252 				// remove selected items
253 				owningView.setTooltip(owningView.hintDeleteId, this, false);
254 				clearMode = false;
255 				owningView.listener.removeItems(owningView.getModel()
256 						.getHighlight(initialItem));
257 				if (!owningView.getHighlight().isEmpty()) {
258 					owningView.getModel().highlight(Interval.EMPTY);
259 					owningView.getModel().notifyModified();
260 				}
261 			} else if ("R".equals(gesture)) {
262 				// select all items
263 				clearMode = false;
264 				if (canHighlight()) {
265 					owningView.setTooltip(R.string.select_all, this, false);
266 					owningView.getModel().highlight(
267 							owningView.getModel().getAllItems());
268 					owningView.getModel().notifyModified();
269 				}
270 			}
271 			enableGestureProcessing = false;
272 			if (clearMode) {
273 				owningView.clearTooltip(this);
274 			}
275 		} else if (g == GestureEnum.ContinuingGesture) {
276 			if ("RU".equals(gesture)) {
277 				// deselect items
278 				owningView.getModel().highlight(Interval.EMPTY);
279 				owningView.getModel().notifyModified();
280 				owningView.setTooltip(R.string.deselect_all, this, false);
281 			} else if ("RD".equals(gesture)) {
282 				// selecting multiple items for sure.
283 				if (owningView.listener.canHighlight()) {
284 					highlighting = true;
285 					owningView.setTooltip(R.string.highlightHint, this, false);
286 					owningView.transparentSelector();
287 				}
288 			} else if("RL".equals(gesture) || "LR".equals(gesture)) {
289 				// cancel
290 				owningView.setTooltip(R.string.cancel, this, false);
291 			} else if (gesture.startsWith("L")
292 					&& canComputeItems()) {
293 				// drag'n'drop / move items up/down
294 				setDragging(true);
295 				final String hint = owningView.listener.getHint(owningView
296 						.getModel().getHighlight(initialItem));
297 				if (hint != null) {
298 					dragHintWindow = new PopupWindow(owningView.getContext());
299 					final TextView textView = new TextView(owningView
300 							.getContext());
301 					textView.setText(hint);
302 					dragHintWindow.setContentView(textView);
303 					dragHintWindow.showAtLocation(owningView.getRootView(),
304 							Gravity.NO_GRAVITY, 0, 0);
305 					dragHintWindow.update(0, 0, LayoutParams.WRAP_CONTENT,
306 							LayoutParams.WRAP_CONTENT);
307 				}
308 				owningView.setTooltip(R.string.dragDrop, this, true);
309 			}
310 			enableGestureProcessing = false;
311 		}
312 	}
313 
314 	private boolean canComputeItems() {
315 		if (owningView.listener.canComputeItems()) {
316 			return true;
317 		}
318 		owningView.setTooltip(R.string.cannotDragDrop, this, false);
319 		return false;
320 	}
321 
322 	private void setDragging(boolean b) {
323 		draggingItems = b;
324 		for (final GesturesListView target : owningView.dragDropViews) {
325 			target.getModel().adapter.setEOP(b);
326 		}
327 	}
328 
329 	/***
330 	 * Activates or deactivates scrolling according to the point location in the
331 	 * view.
332 	 * 
333 	 * @param point
334 	 *            the point in this view's coordinate system
335 	 * @param cancel
336 	 *            if <code>true</code> then scrolling is canceled regardless of
337 	 *            the point location.
338 	 */
339 	private void handleScrolling(final Point point, final boolean cancel) {
340 		if (cancel) {
341 			handler.removeCallbacks(scroller);
342 			scrolling = 0;
343 		} else {
344 			// scroll if the pen is in the bottom area
345 			if (owningView.getHeight() - point.y < 30) {
346 				if (scrolling < 1) {
347 					scrolling = 1;
348 					currentScrolledItem = owningView.pointToPosition(point.x,
349 							point.y)
350 							- scrolling - 1;
351 					handler.post(scroller);
352 				}
353 			} else if (point.y < 30) {
354 				if (scrolling > -1) {
355 					scrolling = -1;
356 					currentScrolledItem = owningView.pointToPosition(point.x,
357 							point.y)
358 							- scrolling + 1;
359 					handler.post(scroller);
360 				}
361 			} else {
362 				if (scrolling != 0) {
363 					handler.removeCallbacks(scroller);
364 					scrolling = 0;
365 				}
366 			}
367 		}
368 	}
369 
370 	private final ViewUtils utils = new ViewUtils();
371 
372 	/***
373 	 * Finds a view containing given point and invokes drag event on the view.
374 	 * The method does nothing if given point does not intersect any registered
375 	 * view.
376 	 * 
377 	 * @param point
378 	 *            the point, relative to this view.
379 	 */
380 	private void invokeDragDropEvent(final Point point) {
381 		final GesturesListView view = owningView.findView(point);
382 		if (view == null)
383 			return;
384 		// we found the view. compute relative position
385 		utils.translateCoordinates(point, owningView, view);
386 		final Point p = ViewUtils.clone(utils.translated);
387 		final Interval hl = owningView.getModel().getHighlight(
388 				initialItem);
389 		// compute item index, where to insert.
390 		int _index = view.pointToPosition(p.x, p.y);
391 		if (_index < 0) {
392 			_index = view.getCount();
393 		}
394 		final int index = _index;
395 		final boolean isMoving = (view == owningView);
396 		if (isMoving) {
397 			final Interval newInterval = view.listener.moveItems(hl, index);
398 			view.getModel().highlight(newInterval);
399 			view.getModel().notifyModified();
400 			return;
401 		}
402 		final boolean isLongOp = owningView.listener
403 				.isComputeTracksLong(hl);
404 		final boolean isOnlineOp = owningView.listener
405 				.isComputeTracksOnlineOp(hl);
406 		// run the drag'n'drop/move operation
407 		final Callable<Void> dragDrop = new Callable<Void>() {
408 			private volatile List<TrackMetadataBean> tracks;
409 
410 			private volatile boolean obtainedTracks = false;
411 
412 			public Void call() {
413 				// drag'n'drop
414 				if (!obtainedTracks) {
415 					tracks = owningView.listener.computeTracks(hl);
416 					obtainedTracks = true;
417 					if (tracks != null) {
418 						// copy items to a thread-safe list
419 						tracks = new CopyOnWriteArrayList<TrackMetadataBean>(
420 								tracks);
421 					}
422 					handler.post(MiscUtils.toRunnable(this));
423 				} else {
424 					if ((tracks != null) && !tracks.isEmpty()) {
425 						view.listener.dropItems(tracks, p.x, p.y, index);
426 					}
427 				}
428 				return null;
429 			}
430 		};
431 		if (isLongOp) {
432 			AmbientApplication.getInstance().getBackgroundTasks().schedule(
433 					dragDrop,
434 					GesturesListView.class,
435 					isOnlineOp,
436 					owningView.getResources().getString(
437 							R.string.fb_adding_tracks));
438 		} else {
439 			try {
440 				dragDrop.call();
441 			} catch (final Exception ex) {
442 				if (!Thread.currentThread().isInterrupted()) {
443 					final AmbientApplication app = AmbientApplication
444 							.getInstance();
445 					app.error(KeypadController.class, true, app
446 							.getString(R.string.error), ex);
447 				}
448 			}
449 		}
450 	}
451 
452 	/***
453 	 * If not <code>0</code> then the {@link #scroller} is active.
454 	 */
455 	private short scrolling = 0;
456 
457 	/***
458 	 * The list view's current selection is set to this item, which causes the
459 	 * list view to be scrolled automatically.
460 	 */
461 	private int currentScrolledItem = 0;
462 
463 	/***
464 	 * Scrolls the view as requested and reschedules itself.
465 	 */
466 	private final Runnable scroller = new Runnable() {
467 		public void run() {
468 			currentScrolledItem += scrolling;
469 			owningView.setSelection(currentScrolledItem);
470 			handler.postDelayed(this, 250);
471 		}
472 	};
473 }