// Copyright (C) 2009 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.client.ui; import com.google.gerrit.client.Gerrit; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.google.gwtexpui.globalkey.client.GlobalKey; import com.google.gwtexpui.globalkey.client.KeyCommand; import com.google.gwtexpui.globalkey.client.KeyCommandSet; import com.google.gwtexpui.safehtml.client.SafeHtml; import java.util.LinkedHashMap; import java.util.Map.Entry; public abstract class NavigationTable<RowItem> extends FancyFlexTable<RowItem> { protected class MyFlexTable extends FancyFlexTable.MyFlexTable { public MyFlexTable() { sinkEvents(Event.ONDBLCLICK | Event.ONCLICK); } @Override public void onBrowserEvent(final Event event) { switch (DOM.eventGetType(event)) { case Event.ONCLICK: { // Find out which cell was actually clicked. final Element td = getEventTargetCell(event); if (td == null) { break; } final int row = rowOf(td); if (getRowItem(row) != null) { onCellSingleClick(event, rowOf(td), columnOf(td)); return; } break; } case Event.ONDBLCLICK: { // Find out which cell was actually clicked. Element td = getEventTargetCell(event); if (td == null) { return; } onCellDoubleClick(rowOf(td), columnOf(td)); return; } } super.onBrowserEvent(event); } } @SuppressWarnings("serial") private static final LinkedHashMap<String, Object> savedPositions = new LinkedHashMap<String, Object>(10, 0.75f, true) { @Override protected boolean removeEldestEntry(Entry<String, Object> eldest) { return size() >= 20; } }; private final Image pointer; protected final KeyCommandSet keysNavigation; protected final KeyCommandSet keysAction; private HandlerRegistration regNavigation; private HandlerRegistration regAction; private int currentRow = -1; private String saveId; private boolean computedScrollType; private ScrollPanel parentScrollPanel; protected NavigationTable(String itemHelpName) { this(); keysNavigation.add( new PrevKeyCommand(0, 'k', Util.M.helpListPrev(itemHelpName)), new NextKeyCommand(0, 'j', Util.M.helpListNext(itemHelpName))); keysNavigation.add(new OpenKeyCommand(0, 'o', Util.M.helpListOpen(itemHelpName))); keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.M.helpListOpen(itemHelpName))); } protected NavigationTable() { pointer = new Image(Gerrit.RESOURCES.arrowRight()); keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation()); keysAction = new KeyCommandSet(Gerrit.C.sectionActions()); } protected abstract void onOpenRow(int row); protected abstract Object getRowItemKey(RowItem item); private void onUp() { for (int row = currentRow - 1; row >= 0; row--) { if (getRowItem(row) != null) { movePointerTo(row); break; } } } private void onDown() { final int max = table.getRowCount(); for (int row = currentRow + 1; row < max; row++) { if (getRowItem(row) != null) { movePointerTo(row); break; } } } private void onOpen() { if (0 <= currentRow && currentRow < table.getRowCount()) { if (getRowItem(currentRow) != null) { onOpenRow(currentRow); } } } /** Invoked when the user double clicks on a table cell. */ protected void onCellDoubleClick(int row, int column) { onOpenRow(row); } /** Invoked when the user clicks on a table cell. */ protected void onCellSingleClick(Event event, int row, int column) { movePointerTo(row); } protected int getCurrentRow() { return currentRow; } protected void ensurePointerVisible() { final int max = table.getRowCount(); int row = currentRow; final int init = row; if (row < 0) { row = 0; } else if (max <= row) { row = max - 1; } final CellFormatter fmt = table.getCellFormatter(); final int sTop = Document.get().getScrollTop(); final int sEnd = sTop + Document.get().getClientHeight(); while (0 <= row && row < max) { final Element cur = fmt.getElement(row, C_ARROW).getParentElement(); final int cTop = cur.getAbsoluteTop(); final int cEnd = cTop + cur.getOffsetHeight(); if (cEnd < sTop) { row++; } else if (sEnd < cTop) { row--; } else { break; } } if (init != row) { movePointerTo(row, false); } } protected void movePointerTo(final int newRow) { movePointerTo(newRow, true); } protected void movePointerTo(final int newRow, final boolean scroll) { final CellFormatter fmt = table.getCellFormatter(); final boolean clear = 0 <= currentRow && currentRow < table.getRowCount(); if (clear) { final Element tr = fmt.getElement(currentRow, C_ARROW).getParentElement(); UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), false); } if (0 <= newRow && newRow < table.getRowCount() && getRowItem(newRow) != null) { table.setWidget(newRow, C_ARROW, pointer); final Element tr = fmt.getElement(newRow, C_ARROW).getParentElement(); UIObject.setStyleName(tr, Gerrit.RESOURCES.css().activeRow(), true); if (scroll && isAttached()) { scrollIntoView(tr); } } else if (clear) { table.setWidget(currentRow, C_ARROW, null); pointer.removeFromParent(); } currentRow = newRow; } protected void scrollIntoView(final Element tr) { if (!computedScrollType) { parentScrollPanel = null; Widget w = getParent(); while (w != null) { if (w instanceof ScrollPanel) { parentScrollPanel = (ScrollPanel) w; break; } w = w.getParent(); } computedScrollType = true; } if (parentScrollPanel != null) { parentScrollPanel.ensureVisible(new UIObject() { { setElement(tr); } }); } else { int rt = tr.getAbsoluteTop(); int rl = tr.getAbsoluteLeft(); int rb = tr.getAbsoluteBottom(); int wt = Window.getScrollTop(); int wl = Window.getScrollLeft(); int wh = Window.getClientHeight(); int ww = Window.getClientWidth(); int wb = wt + wh; // If the row is partially or fully obscured, scroll: // // rl < wl: Row left edge is off screen to left. // rt < wt: Row top is above top of window. // wb < rt: Row top is below bottom of window. // wb < rb: Row bottom is below bottom of window. if (rl < wl || rt < wt || wb < rt || wb < rb) { if (rl < wl) { // Left edge needs to move to make it visible. // If the row fully fits in the window, set 0. if (tr.getAbsoluteRight() < ww) { wl = 0; } else { wl = Math.max(tr.getAbsoluteLeft() - 5, 0); } } // Vertically center the row in the window. int h = (wh - (rb - rt)) / 2; Window.scrollTo(wl, Math.max(rt - h, 0)); } } } protected void movePointerTo(final Object oldId) { final int row = findRow(oldId); if (0 <= row) { movePointerTo(row); } } protected int findRow(final Object oldId) { if (oldId != null) { final int max = table.getRowCount(); for (int row = 0; row < max; row++) { final RowItem c = getRowItem(row); if (c != null && oldId.equals(getRowItemKey(c))) { return row; } } } return -1; } @Override public void resetHtml(SafeHtml body) { currentRow = -1; super.resetHtml(body); } public void finishDisplay() { if (currentRow >= table.getRowCount()) { currentRow = -1; } if (saveId != null) { movePointerTo(savedPositions.get(saveId)); } if (currentRow < 0) { onDown(); } } public void setSavePointerId(final String id) { saveId = id; } public void setRegisterKeys(final boolean on) { if (on && isAttached()) { if (regNavigation == null) { regNavigation = GlobalKey.add(this, keysNavigation); } if (regAction == null) { regAction = GlobalKey.add(this, keysAction); } } else { if (regNavigation != null) { regNavigation.removeHandler(); regNavigation = null; } if (regAction != null) { regAction.removeHandler(); regAction = null; } } } @Override protected void onLoad() { computedScrollType = false; parentScrollPanel = null; } @Override protected void onUnload() { setRegisterKeys(false); if (saveId != null && currentRow >= 0) { final RowItem c = getRowItem(currentRow); if (c != null) { savedPositions.put(saveId, getRowItemKey(c)); } } computedScrollType = false; parentScrollPanel = null; super.onUnload(); } @Override protected MyFlexTable createFlexTable() { return new MyFlexTable(); } public class PrevKeyCommand extends KeyCommand { public PrevKeyCommand(int mask, char key, String help) { super(mask, key, help); } @Override public void onKeyPress(final KeyPressEvent event) { ensurePointerVisible(); onUp(); } } public class NextKeyCommand extends KeyCommand { public NextKeyCommand(int mask, char key, String help) { super(mask, key, help); } @Override public void onKeyPress(final KeyPressEvent event) { ensurePointerVisible(); onDown(); } } public class OpenKeyCommand extends KeyCommand { public OpenKeyCommand(int mask, int key, String help) { super(mask, key, help); } @Override public void onKeyPress(final KeyPressEvent event) { ensurePointerVisible(); onOpen(); } } }