Skip to content

Commit

Permalink
Add DebounceExecutor, update ZoneRenderer.java to use debounced repai…
Browse files Browse the repository at this point in the history
…nt (RPTools#611)

dispatcher.
  • Loading branch information
pnichols04 authored and JamzTheMan committed Sep 10, 2019
1 parent 142205f commit 6604900
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 25 deletions.
116 changes: 116 additions & 0 deletions src/main/java/net/rptools/maptool/client/DebounceExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* This software Copyright by the RPTools.net development team, and
* licensed under the Affero GPL Version 3 or, at your option, any later
* version.
*
* MapTool Source Code is distributed in the hope that it will be
* useful, but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* You should have received a copy of the GNU Affero General Public
* License * along with this source Code. If not, please visit
* <http://www.gnu.org/licenses/> and specifically the Affero license
* text at <http://www.gnu.org/licenses/agpl.html>.
*/
package net.rptools.maptool.client;

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
* Manages throttling rapid successive attempts to execute a task.
*
* @see <a href="https://stackoverflow.com/a/18758408">Cancelling method calls when the same method
* is called multiple time</a>
* @see <a href="https://github.com/ThomasGirard/JDebounce">JDebounce</a>
* @author Philip Nichols ([email protected])
* @version 1.5.3
* @since 2019-08-23
*/
public class DebounceExecutor {

/**
* Thread factory for creating named threads. Other than thread naming, this factory relies on
* defaultThreadFactory and should not add overhead.
*/
private static final ThreadFactory threadFactory =
(new com.google.common.util.concurrent.ThreadFactoryBuilder())
.setNameFormat("debounce-executor-%d")
.build();

/** The time, in milliseconds, during which to throttle subsequent requests to run the task. */
private final long delay;

/**
* A {@link ScheduledExecutorService} that will be used to run the debounced task when the delay
* elapses.
*/
private final ScheduledExecutorService executor =
Executors.newSingleThreadScheduledExecutor(threadFactory);

/** A {@link ScheduledFuture} that represents the debounced task. */
private ScheduledFuture<?> future;

/**
* The synchronization lock used during the critical section for determining how to dispose of any
* single request.
*/
private final Object syncLock = new Object();

/** A {@link Runnable} representing the task to be managed. */
private final Runnable task;

/**
* A {@link long} indicating the time, in milliseconds, when the task last entered a pending
* state.
*/
private long taskPendingSince = -1;

/** A reference to the logging service. */
// private static final Logger log = LogManager.getLogger(DebounceExecutor.class);

/**
* Initializes a new instance of the {@link DebounceExecutor} class.
*
* <p>Note that this class swallows later attempted invocations of its managed task. This makes it
* appropriate for repaint()-like tasks, where even an early-scheduled task will be working with
* the current backbuffer once it finally executes. <i>It is not appropriate for tasks whose
* correct execution relies on swallowing early invocations and running the last-posted one.</i>
*
* @param delay The time, in milliseconds, during which the DebounceExecutor will wait before
* executing the <i>task</i> and throttle subsequent requests.
* @param task The task to be executed after the <i>delay</i> elapses.
*/
public DebounceExecutor(long delay, Runnable task) {
this.delay = delay;
this.task = task;
}

/** Dispatches a task to be executed by this {@link DebounceExecutor} instance. */
public void dispatch() {
if (this.task == null) {
// log.info("Exited debouncer because of a null task.");
return;
}
if (this.delay < 1) {
this.task.run();
return;
}
synchronized (syncLock) {
long now = (new Date()).getTime();
if (this.taskPendingSince == -1 || now - this.taskPendingSince >= this.delay) {
this.taskPendingSince = now;
this.future = this.executor.schedule(this.task, this.delay, TimeUnit.MILLISECONDS);
} /* else {
log.info(
String.format(
"Task execution was debounced. (now: %d; taskPendingSince: %d; delay: %d; now - taskPendingSince: %d)",
now, this.taskPendingSince, this.delay, now - this.taskPendingSince));
} */
}
}
}
58 changes: 33 additions & 25 deletions src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import net.rptools.maptool.client.AppState;
import net.rptools.maptool.client.AppStyle;
import net.rptools.maptool.client.AppUtil;
import net.rptools.maptool.client.DebounceExecutor;
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.client.MapToolUtil;
import net.rptools.maptool.client.ScreenPoint;
Expand Down Expand Up @@ -148,6 +149,13 @@ public class ZoneRenderer extends JComponent
private static final Color TRANSLUCENT_YELLOW =
new Color(Color.yellow.getRed(), Color.yellow.getGreen(), Color.yellow.getBlue(), 50);

/** The interval, in milliseconds, during which calls to repaint() will be debounced. */
private static final int REPAINT_DEBOUNCE_INTERVAL = 33;

/** DebounceExecutor for throttling repaint() requests. */
private final DebounceExecutor repaintDebouncer =
new DebounceExecutor(REPAINT_DEBOUNCE_INTERVAL, this::repaint);

public static final int MIN_GRID_SIZE = 10;
private static LightSourceIconOverlay lightSourceIconOverlay = new LightSourceIconOverlay();
protected Zone zone;
Expand Down Expand Up @@ -310,7 +318,10 @@ public boolean isPathShowing(Token token) {

public void clearShowPaths() {
showPathList.clear();
repaint();
// [PNICHOLS04] This call is unnecessary, because we are in a method that is
// only called once (from clearSelectedTokens), and the caller requires a
// repaint after we return.
// repaintDebouncer.dispatch();
}

public Scale getZoneScale() {
Expand All @@ -332,7 +343,7 @@ public void propertyChange(PropertyChangeEvent evt) {
// flushFog = true;
}
visibleScreenArea = null;
repaint();
repaintDebouncer.dispatch();
}
});
}
Expand All @@ -358,7 +369,7 @@ public void setMouseOver(Token token) {
return;
}
tokenUnderMouse = token;
repaint();
repaintDebouncer.dispatch();
}

@Override
Expand All @@ -375,7 +386,7 @@ public void addMoveSelectionSet(
}
}
selectionSetMap.put(keyToken, new SelectionSet(playerId, keyToken, tokenList));
repaint(); // Jamz: Seems to have no affect?
repaintDebouncer.dispatch(); // Jamz: Seems to have no affect?
}

public boolean hasMoveSelectionSetMoved(GUID keyToken, ZonePoint point) {
Expand All @@ -397,7 +408,7 @@ public void updateMoveSelectionSet(GUID keyToken, ZonePoint offset) {
}
Token token = zone.getToken(keyToken);
set.setOffset(offset.x - token.getX(), offset.y - token.getY());
repaint(); // Jamz: may cause flicker when using AI
repaintDebouncer.dispatch(); // Jamz: may cause flicker when using AI
}

public void toggleMoveSelectionSetWaypoint(GUID keyToken, ZonePoint location) {
Expand All @@ -406,7 +417,7 @@ public void toggleMoveSelectionSetWaypoint(GUID keyToken, ZonePoint location) {
return;
}
set.toggleWaypoint(location);
repaint();
repaintDebouncer.dispatch();
}

public ZonePoint getLastWaypoint(GUID keyToken) {
Expand All @@ -422,7 +433,7 @@ public void removeMoveSelectionSet(GUID keyToken) {
if (set == null) {
return;
}
repaint();
repaintDebouncer.dispatch();
}

public void commitMoveSelectionSet(GUID keyTokenId) {
Expand Down Expand Up @@ -616,8 +627,7 @@ public void centerOn(ZonePoint point) {
y = getSize().height / 2 - (int) (y * getScale()) - 1;

setViewOffset(x, y);

repaint();
repaintDebouncer.dispatch();
}

public void centerOn(CellPoint point) {
Expand Down Expand Up @@ -680,13 +690,13 @@ public void flushLight() {
renderedLightMap = null;
renderedAuraMap = null;
zoneView.flush();
repaint();
repaintDebouncer.dispatch();
}

public void flushFog() {
flushFog = true;
visibleScreenArea = null;
repaint();
repaintDebouncer.dispatch();
}

public Zone getZone() {
Expand Down Expand Up @@ -2738,7 +2748,7 @@ public void setActiveLayer(Zone.Layer layer) {
if (!keepSelectedTokenSet) selectedTokenSet.clear();
else keepSelectedTokenSet = false; // Always reset it back, temp boolean only

repaint();
repaintDebouncer.dispatch();
}

/**
Expand Down Expand Up @@ -3649,7 +3659,7 @@ public void deselectToken(GUID tokenGUID) {
// flushFog = true; // could call flushFog() but also clears visibleScreenArea and I don't know
// if we want
// that...
repaint();
repaintDebouncer.dispatch();
}

public boolean selectToken(GUID tokenGUID) {
Expand All @@ -3661,7 +3671,7 @@ public boolean selectToken(GUID tokenGUID) {
MapTool.getFrame().resetTokenPanels();
HTMLFrameFactory.selectedListChanged();
// flushFog = true;
repaint();
repaintDebouncer.dispatch();
return true;
}

Expand All @@ -3674,7 +3684,7 @@ public void selectTokens(Collection<GUID> tokens) {
}
addToSelectionHistory(selectedTokenSet);

repaint();
repaintDebouncer.dispatch();
MapTool.getFrame().resetTokenPanels();
HTMLFrameFactory.selectedListChanged();
}
Expand All @@ -3696,7 +3706,7 @@ public void clearSelectedTokens() {
selectedTokenSet.clear();
MapTool.getFrame().resetTokenPanels();
HTMLFrameFactory.selectedListChanged();
repaint();
repaintDebouncer.dispatch();
}

public void undoSelectToken() {
Expand Down Expand Up @@ -3729,7 +3739,7 @@ public void undoSelectToken() {
// disable the undo button.
MapTool.getFrame().resetTokenPanels();
HTMLFrameFactory.selectedListChanged();
repaint();
repaintDebouncer.dispatch();
}

private void addToSelectionHistory(Set<GUID> selectionSet) {
Expand Down Expand Up @@ -3900,7 +3910,7 @@ public int getViewOffsetY() {

public void adjustGridSize(int delta) {
zone.getGrid().setSize(Math.max(0, zone.getGrid().getSize() + delta));
repaint();
repaintDebouncer.dispatch();
}

public void moveGridBy(int dx, int dy) {
Expand All @@ -3917,7 +3927,7 @@ public void moveGridBy(int dx, int dy) {
gridOffsetX = gridOffsetX - (int) zone.getGrid().getCellWidth();
}
zone.getGrid().setOffset(gridOffsetX, gridOffsetY);
repaint();
repaintDebouncer.dispatch();
}

/**
Expand Down Expand Up @@ -3956,7 +3966,7 @@ public double getScaledGridSize() {
/** This makes sure that any image updates get refreshed. This could be a little smarter. */
@Override
public boolean imageUpdate(Image img, int infoflags, int x, int y, int w, int h) {
repaint();
repaintDebouncer.dispatch();
return super.imageUpdate(img, infoflags, x, y, w, h);
}

Expand Down Expand Up @@ -4166,8 +4176,6 @@ public void toggleWaypoint(ZonePoint location) {
/**
* Retrieves the last waypoint, or if there isn't one then the start point of the first path
* segment.
*
* @param location
*/
public ZonePoint getLastWaypoint() {
ZonePoint zp;
Expand Down Expand Up @@ -4464,7 +4472,7 @@ private void addTokens(
AppActions.copyTokens(tokens);
AppActions.updateActions();
requestFocusInWindow();
repaint();
repaintDebouncer.dispatch();
}

/**
Expand Down Expand Up @@ -4552,7 +4560,7 @@ public void modelChanged(ModelChangeEvent event) {
flushFog = true;
}
MapTool.getFrame().updateTokenTree();
repaint();
repaintDebouncer.dispatch();
}
}

Expand All @@ -4574,7 +4582,7 @@ public List<Token> getHighlightCommonMacros() {

public void setHighlightCommonMacros(List<Token> affectedTokens) {
highlightCommonMacros = affectedTokens;
repaint();
repaintDebouncer.dispatch();
}

// End token common macro identification
Expand Down

0 comments on commit 6604900

Please sign in to comment.