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
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
187
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
225
226 suppressHandler = true;
227 if ((firstGesture == MouseGesturesRecognizer.DOWN_MOVE)
228 || (firstGesture == MouseGesturesRecognizer.UP_MOVE)) {
229
230
231 enableGestureProcessing = false;
232 suppressHandler = false;
233 } else if (firstGesture == MouseGesturesRecognizer.RIGHT_MOVE) {
234
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
248 }
249 } else if (g == GestureEnum.GestureFinished) {
250 boolean clearMode = true;
251 if ("L".equals(gesture)) {
252
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
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
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
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
290 owningView.setTooltip(R.string.cancel, this, false);
291 } else if (gesture.startsWith("L")
292 && canComputeItems()) {
293
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
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
385 utils.translateCoordinates(point, owningView, view);
386 final Point p = ViewUtils.clone(utils.translated);
387 final Interval hl = owningView.getModel().getHighlight(
388 initialItem);
389
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
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
414 if (!obtainedTracks) {
415 tracks = owningView.listener.computeTracks(hl);
416 obtainedTracks = true;
417 if (tracks != null) {
418
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 }