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.Collections;
22  import java.util.List;
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 android.view.KeyEvent;
29  
30  /***
31   * Controls the {@link GesturesListView} component via the keypad.
32   * 
33   * @author Martin Vysny
34   */
35  final class KeypadController implements KeyEvent.Callback {
36  	/***
37  	 * The owning view.
38  	 */
39  	private final GesturesListView ownerView;
40  
41  	/***
42  	 * Creates new controller instance.
43  	 * 
44  	 * @param ownerView
45  	 *            the owning view.
46  	 */
47  	KeypadController(final GesturesListView ownerView) {
48  		super();
49  		this.ownerView = ownerView;
50  	}
51  
52  	/***
53  	 * The mode we are currently in.
54  	 * 
55  	 * @author Martin Vysny
56  	 */
57  	private static enum Mode {
58  		/***
59  		 * Normal mode.
60  		 */
61  		NORMAL,
62  		/***
63  		 * Selection mode.Items are selected by pressing the U , D arrow keys.
64  		 * To leave this mode, press Center button. Pressing the Center button
65  		 * copies selected items into the clipboard.
66  		 */
67  		SELECTION,
68  		/***
69  		 * Selected items are moved up/downwards the playlist as the U and D
70  		 * buttons are pressed. To leave this mode, press Center button.
71  		 */
72  		MOVE;
73  	}
74  
75  	/***
76  	 * Current working mode.
77  	 */
78  	private Mode mode = Mode.NORMAL;
79  
80  	/***
81  	 * Previous key event. Serves for remembering gesture combo.
82  	 */
83  	private int prevKeyCode = -1;
84  
85  	/***
86  	 * Initial index of the item when selecting items.
87  	 */
88  	private int initialItem = -1;
89  
90  	private boolean isReadOnly() {
91  		if (!ownerView.listener.isReadOnly()) {
92  			return false;
93  		}
94  		ownerView.setMode(R.string.cannotModify, this, false);
95  		initialItem = -1;
96  		prevKeyCode = -1;
97  		mode = Mode.NORMAL;
98  		return true;
99  	}
100 
101 	private boolean canHighlight() {
102 		if (ownerView.listener.canHighlight()) {
103 			return true;
104 		}
105 		ownerView.setMode(R.string.cannotSelect, this, false);
106 		initialItem = -1;
107 		prevKeyCode = -1;
108 		mode = Mode.NORMAL;
109 		return false;
110 	}
111 
112 	public boolean onKeyUp(int keyCode, KeyEvent event) {
113 		final int index = ownerView.getSelectedItemPosition();
114 		final boolean isEOP = ownerView.isEOP(index);
115 		switch (mode) {
116 		case NORMAL:
117 			switch (keyCode) {
118 			case KeyEvent.KEYCODE_DPAD_CENTER:
119 				boolean clearHint = true;
120 				if (index >= 0) {
121 					if (prevKeyCode == -1) {
122 						if (!isEOP) {
123 							ownerView.listener.itemActivated(index, ownerView
124 									.getModel().getModel().get(index));
125 						}
126 					} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
127 						// the LC gesture. paste
128 						clearHint = false;
129 						if (!isReadOnly()) {
130 							final List<TrackMetadataBean> tracks = getClipboard();
131 							if (!tracks.isEmpty()) {
132 								ownerView.setMode(R.string.paste, this, false);
133 								ownerView.listener.dropItems(tracks, -1, -1,
134 										index);
135 							} else {
136 								ownerView.setMode(R.string.clipboardEmpty,
137 										this, false);
138 							}
139 						}
140 					} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
141 						// copy to clipboard
142 						if (ownerView.listener.canComputeItems()) {
143 							clearHint = false;
144 							ownerView.setMode(R.string.copyToClipboard, this,
145 									false);
146 							copy(ownerView.getModel().getHighlight(initialItem));
147 						}
148 					}
149 				}
150 				clearMode(clearHint);
151 				return true;
152 			case KeyEvent.KEYCODE_DPAD_RIGHT:
153 				if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
154 					// RR gesture - enter selection mode
155 					if (canHighlight()) {
156 						ownerView.getModel().highlight(
157 								ownerView.getModel().getAllItems());
158 						ownerView.getModel().notifyModified();
159 						mode = Mode.SELECTION;
160 						prevKeyCode = -1;
161 					}
162 					return true;
163 				}
164 				if (index >= 0) {
165 					if (canHighlight()) {
166 						prevKeyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
167 						ownerView.setMode(R.string.selection, this, true);
168 						ownerView.getModel()
169 								.highlight(Interval.fromItem(index));
170 						ownerView.getModel().notifyModified();
171 						initialItem = index;
172 					}
173 					return true;
174 				}
175 				break;
176 			case KeyEvent.KEYCODE_DPAD_DOWN:
177 				if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
178 					// RD - enter selection mode
179 					if (canHighlight()) {
180 						mode = Mode.SELECTION;
181 						prevKeyCode = -1;
182 					}
183 					// fall back to the original implementation, to fire
184 					// selectionChanged event
185 					return false;
186 				} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
187 					// LD - move up
188 					if (!isReadOnly()) {
189 						enterItemMoveMode(isEOP, true);
190 					}
191 					return true;
192 				}
193 				break;
194 			case KeyEvent.KEYCODE_DPAD_UP:
195 				if (prevKeyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
196 					// RU - clear selection
197 					ownerView.setMode(R.string.deselect_all,
198 							KeypadController.this, false);
199 					ownerView.getModel().highlight(Interval.EMPTY);
200 					ownerView.getModel().notifyModified();
201 					clearMode(false);
202 					return true;
203 				} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
204 					// LU - move up
205 					if (!isReadOnly()) {
206 						enterItemMoveMode(isEOP, false);
207 					}
208 					return true;
209 				}
210 				break;
211 			case KeyEvent.KEYCODE_DPAD_LEFT:
212 				if (prevKeyCode == -1) {
213 					prevKeyCode = KeyEvent.KEYCODE_DPAD_LEFT;
214 					ownerView.setMode(ownerView.hintDeleteMovePasteId, this,
215 							true);
216 					return true;
217 				} else if (prevKeyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
218 					// LL - delete highlight
219 					ownerView.setMode(ownerView.hintDeleteId, this, false);
220 					ownerView.listener.removeItems(ownerView.getModel()
221 							.getHighlight(true));
222 					if (!ownerView.getHighlight().isEmpty()) {
223 						ownerView.getModel().highlight(Interval.EMPTY);
224 						ownerView.getModel().notifyModified();
225 					}
226 					clearMode(false);
227 					return true;
228 				}
229 				break;
230 			default:
231 				clearMode();
232 			}
233 			break;
234 		case SELECTION:
235 			// we do not handle UP/DOWN keys - we'll handle the selectionChanged
236 			// event instead. The only button we handle is the Center button,
237 			// which leaves this mode, and the R button, which selects all
238 			// items.
239 			switch (keyCode) {
240 			case KeyEvent.KEYCODE_DPAD_CENTER:
241 				ownerView.setMode(R.string.copyToClipboard, this, false);
242 				copy(ownerView.getModel().getHighlight(initialItem));
243 				clearMode(false);
244 				return true;
245 			case KeyEvent.KEYCODE_DPAD_RIGHT:
246 				ownerView.getModel().highlight(
247 						ownerView.getModel().getAllItems());
248 				ownerView.getModel().notifyModified();
249 				return true;
250 			}
251 			break;
252 		case MOVE:
253 			switch (keyCode) {
254 			case KeyEvent.KEYCODE_DPAD_CENTER:
255 				clearMode();
256 				return true;
257 			case KeyEvent.KEYCODE_DPAD_UP: {
258 				final Interval newInterval = moveItems(false);
259 				ownerView.getModel().highlight(newInterval);
260 				ownerView.getModel().notifyModified();
261 				ownerView.setSelection(newInterval.start);
262 				return true;
263 			}
264 			case KeyEvent.KEYCODE_DPAD_DOWN: {
265 				final Interval newInterval = moveItems(true);
266 				ownerView.getModel().highlight(newInterval);
267 				ownerView.getModel().notifyModified();
268 				ownerView.setSelection(newInterval.end);
269 				return true;
270 			}
271 			}
272 			break;
273 		}
274 		return false;
275 	}
276 
277 	private Interval enterItemMoveMode(final boolean isEOP, final boolean down) {
278 		if (!ownerView.canMove() || isEOP) {
279 			clearMode(true);
280 			return ownerView.getHighlight();
281 		}
282 		mode = Mode.MOVE;
283 		ownerView.setMode(R.string.move, this, true);
284 		final Interval result = moveItems(down);
285 		ownerView.getModel().notifyModified();
286 		return result;
287 	}
288 
289 	private Interval moveItems(final boolean down) {
290 		return ownerView.listener.moveItemsByOne(ownerView.getModel()
291 				.getHighlight(true), down);
292 	}
293 
294 	private void clearMode() {
295 		clearMode(true);
296 	}
297 
298 	private void clearMode(final boolean clearModeHint) {
299 		mode = Mode.NORMAL;
300 		if (clearModeHint)
301 			ownerView.clearMode(this);
302 		prevKeyCode = -1;
303 	}
304 
305 	public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
306 		return false;
307 	}
308 
309 	public boolean onKeyMultiple(int keyCode, int arg1,
310 			android.view.KeyEvent event) {
311 		// do nothing
312 		return false;
313 	}
314 
315 	/***
316 	 * Invoked when the selection is changed.
317 	 */
318 	synchronized void selectionChanged() {
319 		switch (mode) {
320 		case NORMAL:
321 			// do nothing
322 			return;
323 		case SELECTION:
324 			final int select = ownerView.getSelectedItemPosition();
325 			if (select >= 0) {
326 				ownerView.getModel().highlight(
327 						Interval.fromRange(select, initialItem));
328 				ownerView.getModel().notifyModified();
329 			}
330 			return;
331 		case MOVE:
332 			return;
333 		}
334 	}
335 
336 	/***
337 	 * Invoked by gestures list view if this controller should cancel all its
338 	 * work immediately as other controller is about to do something.
339 	 */
340 	synchronized void cancelWork() {
341 		clearMode();
342 	}
343 
344 	/***
345 	 * Copies given interval into the clipboard. The operation does not block -
346 	 * it may retrieve the tracks in the background
347 	 * 
348 	 * @param i
349 	 *            the interval to copy.
350 	 */
351 	public void copy(final Interval i) {
352 		final boolean isLongOp = ownerView.listener.isComputeTracksLong(i);
353 		final boolean isOnlineOp = ownerView.listener
354 				.isComputeTracksOnlineOp(i);
355 		final Runnable copyOp = new Runnable() {
356 			/***
357 			 * Retrieved tracks.
358 			 */
359 			private volatile List<TrackMetadataBean> tracks = null;
360 
361 			public void run() {
362 				if (tracks == null) {
363 					tracks = ownerView.listener.computeTracks(i);
364 					if (Thread.currentThread().isInterrupted())
365 						return;
366 					if ((tracks == null) || tracks.isEmpty())
367 						return;
368 					// prepare a thread-safe list
369 					tracks = Collections.synchronizedList(tracks);
370 					AmbientApplication.getHandler().post(this);
371 				} else {
372 					if (ownerView.dragDropViews.isEmpty())
373 						return;
374 					final TrackListClipboardObject obj = new TrackListClipboardObject(
375 							tracks, ownerView.dragDropViews);
376 					ownerView.listener.setClipboard(obj);
377 				}
378 			}
379 		};
380 		if (isLongOp) {
381 			AmbientApplication.getInstance().getBackgroundTasks().schedule(
382 					copyOp,
383 					GesturesListView.class,
384 					isOnlineOp,
385 					ownerView.getResources().getString(
386 							R.string.copyingToClipboard));
387 		} else {
388 			try {
389 				copyOp.run();
390 			} catch (final Exception ex) {
391 				if (!Thread.currentThread().isInterrupted()) {
392 					final AmbientApplication app = AmbientApplication
393 							.getInstance();
394 					app.error(KeypadController.class, true, app
395 							.getString(R.string.error), ex);
396 				}
397 			}
398 		}
399 	}
400 
401 	/***
402 	 * Returns the contents of the clipboard as a list of tracks.
403 	 * 
404 	 * @return list of tracks, never <code>null</code>, may be empty.
405 	 */
406 	public List<TrackMetadataBean> getClipboard() {
407 		final TrackListClipboardObject obj = ownerView.getClipboard();
408 		if (obj == null)
409 			return Collections.emptyList();
410 		if (!obj.isTarget(ownerView))
411 			return Collections.emptyList();
412 		return obj.getObjects();
413 	}
414 }