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