mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-10 06:02:09 +00:00
GP-4861 - Created a way to show a message over a given component
This commit is contained in:
parent
187406f45b
commit
0c365b7afd
@ -16,6 +16,7 @@
|
||||
package ghidra.features.base.memsearch.gui;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -29,6 +30,7 @@ import docking.action.DockingAction;
|
||||
import docking.action.ToggleDockingAction;
|
||||
import docking.action.builder.ActionBuilder;
|
||||
import docking.action.builder.ToggleActionBuilder;
|
||||
import docking.util.GGlassPaneMessage;
|
||||
import generic.theme.GIcon;
|
||||
import ghidra.app.context.NavigatableActionContext;
|
||||
import ghidra.app.nav.Navigatable;
|
||||
@ -100,6 +102,9 @@ public class MemorySearchProvider extends ComponentProviderAdapter
|
||||
private SearchGuiModel model;
|
||||
private boolean isPrivate = false;
|
||||
|
||||
// used to show a temporary message over the table
|
||||
private GGlassPaneMessage glassPaneMessage;
|
||||
|
||||
public MemorySearchProvider(MemorySearchPlugin plugin, Navigatable navigatable,
|
||||
SearchSettings settings, MemorySearchOptions options, SearchHistory history) {
|
||||
super(plugin.getTool(), "Memory Search", plugin.getName());
|
||||
@ -355,9 +360,9 @@ public class MemorySearchProvider extends ComponentProviderAdapter
|
||||
setBusy(false);
|
||||
updateSubTitle();
|
||||
if (!cancelled && terminatedEarly) {
|
||||
Msg.showInfo(getClass(), resultsPanel, "Search Limit Exceeded!",
|
||||
"Stopped search after finding " + options.getSearchLimit() + " matches.\n" +
|
||||
"The search limit can be changed at Edit->Tool Options, under Search.");
|
||||
showAlert("Search Limit Exceeded!\n\nStopped search after finding " +
|
||||
options.getSearchLimit() + " matches.\n" +
|
||||
"The search limit can be changed at Edit \u2192 Tool Options, under Search.");
|
||||
|
||||
}
|
||||
else if (!foundResults) {
|
||||
@ -545,6 +550,8 @@ public class MemorySearchProvider extends ComponentProviderAdapter
|
||||
}
|
||||
|
||||
private void dispose() {
|
||||
glassPaneMessage.hide();
|
||||
glassPaneMessage = null;
|
||||
matchHighlighter.dispose();
|
||||
USED_IDS.remove(id);
|
||||
if (navigatable != null) {
|
||||
@ -638,12 +645,6 @@ public class MemorySearchProvider extends ComponentProviderAdapter
|
||||
return byteMatcher.getDescription();
|
||||
}
|
||||
|
||||
void showAlert(String alertMessage) {
|
||||
// replace with water mark concept
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
Msg.showInfo(this, null, "Search Results", alertMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ActionContext createContext(Component sourceComponent, Object contextObject) {
|
||||
ActionContext context = new NavigatableActionContext(this, navigatable);
|
||||
@ -652,4 +653,16 @@ public class MemorySearchProvider extends ComponentProviderAdapter
|
||||
return context;
|
||||
}
|
||||
|
||||
private void showAlert(String message) {
|
||||
Toolkit.getDefaultToolkit().beep();
|
||||
|
||||
if (glassPaneMessage == null) {
|
||||
GhidraTable table = resultsPanel.getTable();
|
||||
glassPaneMessage = new GGlassPaneMessage(table);
|
||||
glassPaneMessage.setHideDelay(Duration.ofSeconds(3));
|
||||
}
|
||||
|
||||
glassPaneMessage.showCenteredMessage(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,8 @@ color.fg.dialog.status.error = color.fg.messages.error
|
||||
color.fg.dialog.status.normal = color.fg.messages.normal
|
||||
color.fg.dialog.status.warning = color.fg.messages.warning
|
||||
|
||||
color.fg.glasspane.message = color.palette.lightcornflowerblue
|
||||
|
||||
color.bg.selection = color.palette.palegreen
|
||||
color.bg.highlight = color.palette.lemonchiffon
|
||||
|
||||
@ -125,9 +127,6 @@ icon.widget.tabs.empty.small = empty8x16.png
|
||||
icon.widget.tabs.close = x.gif
|
||||
icon.widget.tabs.close.highlight = pinkX.gif
|
||||
icon.widget.tabs.list = VCRFastForward.gif
|
||||
font.widget.tabs.selected = sansserif-plain-11
|
||||
font.widget.tabs = sansserif-plain-11
|
||||
font.widget.tabs.list = sansserif-bold-9
|
||||
|
||||
icon.dialog.error.expandable.report = icon.spreadsheet
|
||||
icon.dialog.error.expandable.exception = program_obj.png
|
||||
@ -157,6 +156,10 @@ icon.task.progress.hourglass.11 = hourglass24_11.png
|
||||
|
||||
// Fonts
|
||||
|
||||
font.input.hint = monospaced-plain-10
|
||||
|
||||
font.glasspane.message = sansserif-bold-24
|
||||
|
||||
font.splash.header.default = serif-bold-35
|
||||
font.splash.status = serif-bold-12
|
||||
|
||||
@ -164,12 +167,13 @@ font.splash.status = serif-bold-12
|
||||
font.table.base = [font]system.font.control
|
||||
font.table.header.number = arial-bold-12
|
||||
|
||||
font.input.hint = monospaced-plain-10
|
||||
|
||||
font.task.monitor.label.message = sansserif-plain-10
|
||||
|
||||
font.wizard.border.title = sansserif-plain-10
|
||||
font.widget.tabs.selected = sansserif-plain-11
|
||||
font.widget.tabs = sansserif-plain-11
|
||||
font.widget.tabs.list = sansserif-bold-9
|
||||
|
||||
font.wizard.border.title = sansserif-plain-10
|
||||
|
||||
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* 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.
|
||||
@ -16,10 +15,10 @@
|
||||
*/
|
||||
package docking.util;
|
||||
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
|
||||
import java.awt.Graphics;
|
||||
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
|
||||
/**
|
||||
* An interface used with {@link AnimationUtils} to allow clients to use the timing
|
||||
* framework while performing their own painting.
|
||||
@ -31,7 +30,7 @@ public interface AnimationPainter {
|
||||
*
|
||||
* @param glassPane the glass pane upon which painting takes place
|
||||
* @param graphics the graphics used to paint
|
||||
* @param percentComplete a value from 0 to 1, 1 being fully complete.
|
||||
* @param value a value from from the range supplied to the animator when it was created
|
||||
*/
|
||||
public void paint(GGlassPane glassPane, Graphics graphics, double percentComplete);
|
||||
public void paint(GGlassPane glassPane, Graphics graphics, double value);
|
||||
}
|
||||
|
@ -0,0 +1,256 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 docking.util;
|
||||
|
||||
import java.awt.Graphics;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
|
||||
import org.jdesktop.animation.timing.Animator;
|
||||
import org.jdesktop.animation.timing.TimingTargetAdapter;
|
||||
import org.jdesktop.animation.timing.interpolation.PropertySetter;
|
||||
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import ghidra.util.bean.GGlassPanePainter;
|
||||
import utility.function.Callback;
|
||||
import utility.function.Dummy;
|
||||
|
||||
/**
|
||||
* A class that does basic setup work for creating an {@link Animator}. The animator will run a
|
||||
* timer in a background thread, calling the client periodically until the animation progress is
|
||||
* finished. The actual visual animation is handled by the client's {@link AnimationPainter}.
|
||||
* This class is provided for convenience. Clients can create their own {@link Animator} as needed.
|
||||
* <P>
|
||||
* A {@link #setPainter(AnimationPainter) painter} must be supplied before calling {@link #start()}.
|
||||
* A simple example usage:
|
||||
* <PRE>
|
||||
* GTable table = ...;
|
||||
* AnimationPainter painter = new AnimationPainter() {
|
||||
* public void paint(GGlassPane glassPane, Graphics graphics, double value) {
|
||||
*
|
||||
* // repaint some contents to the glass pane's graphics using the current value as to
|
||||
* // know where we are in the progress of animating
|
||||
* }
|
||||
* };
|
||||
* AnimationRunner animation = new AnimationRunner(table);
|
||||
* animation.setPainter(painter);
|
||||
* animation.start();
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // code to stop animation, such as when a request for a new animation is received
|
||||
* if (animation != null) {
|
||||
* animation.stop();
|
||||
* }
|
||||
*
|
||||
* </PRE>
|
||||
* <P>
|
||||
* Clients who wish to perform more configuration can call {@link #createAnimator()} to perform the
|
||||
* basic setup, calling {@link #start()} when finished with any follow-up configuration.
|
||||
* <P>
|
||||
* See {@link Animator} for details on the animation process.
|
||||
*/
|
||||
public class AnimationRunner {
|
||||
|
||||
private JComponent component;
|
||||
private GGlassPane glassPane;
|
||||
private Animator animator;
|
||||
private UserDefinedPainter painter;
|
||||
private boolean removePainterWhenFinished = true;
|
||||
private Callback doneCallback = Callback.dummy();
|
||||
|
||||
private Duration duration = Duration.ofSeconds(1);
|
||||
private Double[] values = new Double[] { 0D, 1D };
|
||||
|
||||
public AnimationRunner(JComponent component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the painter required for the animator to work.
|
||||
* @param animationPainter the painter.
|
||||
*/
|
||||
public void setPainter(AnimationPainter animationPainter) {
|
||||
this.painter = new UserDefinedPainter(animationPainter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the values passed to the animator created by this class. These values will be split
|
||||
* into a range of values, broken up by the duration of the animator. The default values are 0
|
||||
* and 1.
|
||||
* <P>
|
||||
* See {@link PropertySetter#createAnimator(int, Object, String, Object...)}.
|
||||
* @param values the values
|
||||
*/
|
||||
public void setValues(Double... values) {
|
||||
if (values == null || values.length == 0) {
|
||||
throw new IllegalArgumentException("'values' cannot be null or empty");
|
||||
}
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to remove the painter from the glass pane when the animation is finished. Clients
|
||||
* can specify {@code false} which will allow the painting to continue after the animation has
|
||||
* finished.
|
||||
* @param b true to remove the painter. The default value is true.
|
||||
*/
|
||||
public void setRemovePainterWhenFinished(boolean b) {
|
||||
this.removePainterWhenFinished = b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be called when the animation is finished.
|
||||
* @param c the callback
|
||||
*/
|
||||
public void setDoneCallback(Callback c) {
|
||||
this.doneCallback = Dummy.ifNull(c);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the animation duration. The default is 1 second.
|
||||
* @param duration the duration
|
||||
*/
|
||||
public void setDuration(Duration duration) {
|
||||
this.duration = Objects.requireNonNull(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a method used by the animator. Clients should not call this method.
|
||||
* @param value the current value created by the animator
|
||||
*/
|
||||
public void setCurrentValue(Double value) {
|
||||
painter.setValue(value);
|
||||
glassPane.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the animator used to perform animation. Clients will call {@link Animator#start()}
|
||||
* to begin animation. Many attributes of the animator can be configured before starting.
|
||||
* @return the animator
|
||||
* @throws IllegalStateException if require values have not been set on this class, such as
|
||||
* {@link #setValues(Double...)} or {@link #setPainter(AnimationPainter)}.
|
||||
*/
|
||||
public Animator createAnimator() {
|
||||
|
||||
if (animator != null) {
|
||||
return animator;
|
||||
}
|
||||
|
||||
glassPane = AnimationUtils.getGlassPane(component);
|
||||
if (glassPane == null) {
|
||||
Msg.debug(AnimationUtils.class,
|
||||
"Cannot animate without a " + GGlassPane.class.getName() + " installed");
|
||||
throw new IllegalStateException("Unable to find Glass Pane");
|
||||
}
|
||||
|
||||
if (painter == null) {
|
||||
throw new IllegalStateException("A painter must be supplied");
|
||||
}
|
||||
|
||||
setCurrentValue(values[0]); // set initial value
|
||||
|
||||
int aniationDuration = (int) duration.toMillis();
|
||||
if (!AnimationUtils.isAnimationEnabled()) {
|
||||
aniationDuration = 0; // do not animate
|
||||
}
|
||||
|
||||
animator = PropertySetter.createAnimator(aniationDuration, this, "currentValue", values);
|
||||
animator.setAcceleration(0.2f);
|
||||
animator.setDeceleration(0.8f);
|
||||
animator.addTarget(new TimingTargetAdapter() {
|
||||
@Override
|
||||
public void end() {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
return animator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the animation process, creating the animator as needed. This method can be called
|
||||
* repeatedly without calling stop first.
|
||||
*/
|
||||
public void start() {
|
||||
|
||||
if (painter == null) {
|
||||
throw new IllegalStateException("A painter must be supplied");
|
||||
}
|
||||
|
||||
if (animator != null) {
|
||||
if (animator.isRunning()) {
|
||||
animator.stop();
|
||||
}
|
||||
}
|
||||
else {
|
||||
animator = createAnimator();
|
||||
}
|
||||
|
||||
glassPane.addPainter(painter);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all animation and removes the painter from the glass pane. {@link #start()} can be
|
||||
* called again after calling this method.
|
||||
*/
|
||||
public void stop() {
|
||||
if (animator != null) {
|
||||
animator.stop();
|
||||
}
|
||||
|
||||
glassPane.removePainter(painter);
|
||||
}
|
||||
|
||||
private void done() {
|
||||
setCurrentValue(values[values.length - 1]);
|
||||
|
||||
if (removePainterWhenFinished) {
|
||||
glassPane.removePainter(painter);
|
||||
}
|
||||
else {
|
||||
glassPane.repaint();
|
||||
}
|
||||
|
||||
doneCallback.call();
|
||||
}
|
||||
|
||||
/**
|
||||
* A painter that will call the user-supplied painter with the current value.
|
||||
*/
|
||||
private class UserDefinedPainter implements GGlassPanePainter {
|
||||
|
||||
private AnimationPainter userPainter;
|
||||
private double value;
|
||||
|
||||
UserDefinedPainter(AnimationPainter userPainter) {
|
||||
this.userPainter = userPainter;
|
||||
}
|
||||
|
||||
void setValue(double value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paint(GGlassPane gp, Graphics graphics) {
|
||||
userPainter.paint(gp, graphics, value);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@ -133,24 +133,19 @@ public class AnimationUtils {
|
||||
return driver.animator;
|
||||
}
|
||||
|
||||
public static Animator createPaintingAnimator(Component window, AnimationPainter painter) {
|
||||
public static Animator createPaintingAnimator(Component component, AnimationPainter painter) {
|
||||
if (!animationEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Component paneComponent = getGlassPane(window);
|
||||
if (paneComponent == null) {
|
||||
GGlassPane glassPane = getGlassPane(component);
|
||||
if (glassPane == null) {
|
||||
// could happen if the given component has not yet been realized
|
||||
Msg.debug(AnimationUtils.class,
|
||||
"Cannot animate without a " + GGlassPane.class.getName() + " installed");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(paneComponent instanceof GGlassPane)) {
|
||||
Msg.debug(AnimationUtils.class,
|
||||
"Cannot animate without a " + GGlassPane.class.getName() + " installed");
|
||||
return null; // shouldn't happen
|
||||
}
|
||||
|
||||
GGlassPane glassPane = (GGlassPane) paneComponent;
|
||||
BasicAnimationDriver driver =
|
||||
new BasicAnimationDriver(glassPane, new UserDefinedPainter(painter));
|
||||
return driver.animator;
|
||||
@ -596,6 +591,8 @@ public class AnimationUtils {
|
||||
|
||||
animator = PropertySetter.createAnimator(2000, this, "percentComplete", start, max);
|
||||
|
||||
painter.setPercentComplete(1D); // set initial value
|
||||
|
||||
animator.setAcceleration(0.2f);
|
||||
animator.setDeceleration(0.8f);
|
||||
animator.addTarget(new TimingTargetAdapter() {
|
||||
|
@ -0,0 +1,362 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 docking.util;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import generic.json.Json;
|
||||
import generic.theme.Gui;
|
||||
import generic.util.WindowUtilities;
|
||||
import generic.util.image.ImageUtils;
|
||||
import ghidra.util.Swing;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
import ghidra.util.timer.GTimer;
|
||||
import ghidra.util.timer.GTimerMonitor;
|
||||
|
||||
/**
|
||||
* A class that allows clients to paint a message over top of a given component.
|
||||
* <P>
|
||||
* This class will honor newline characters and will word wrap as needed. If the message being
|
||||
* displayed will not fit within the bounds of the given component, then the text will be clipped.
|
||||
*/
|
||||
public class GGlassPaneMessage {
|
||||
|
||||
private static final int HIDE_DELAY_MILLIS = 2000;
|
||||
|
||||
private AnimationRunner animationRunner;
|
||||
private GTimerMonitor timerMonitor;
|
||||
private Duration hideDelay = Duration.ofMillis(HIDE_DELAY_MILLIS);
|
||||
|
||||
private JComponent component;
|
||||
private String message;
|
||||
|
||||
public GGlassPaneMessage(JComponent component) {
|
||||
this.component = component;
|
||||
|
||||
component.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
|
||||
if (animationRunner != null) {
|
||||
hide();
|
||||
e.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the amount of time the message will remain on screen after the animation has completed.
|
||||
* To hide the message sooner, call {@link #hide()}.
|
||||
* @param duration the duration
|
||||
*/
|
||||
public void setHideDelay(Duration duration) {
|
||||
hideDelay = Objects.requireNonNull(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the given message centered over the component used by this class.
|
||||
* @param newMessage the message
|
||||
*/
|
||||
public void showCenteredMessage(String newMessage) {
|
||||
AnimationPainter painter = new CenterTextPainter();
|
||||
showMessage(newMessage, painter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a message at the bottom of the component used by this class.
|
||||
* @param newMessage the message
|
||||
*/
|
||||
public void showBottomMessage(String newMessage) {
|
||||
AnimationPainter painter = new BottomTextPainter();
|
||||
showMessage(newMessage, painter);
|
||||
}
|
||||
|
||||
public void showMessage(String newMessage, AnimationPainter painter) {
|
||||
|
||||
hide();
|
||||
|
||||
this.message = Objects.requireNonNull(newMessage);
|
||||
|
||||
AnimationRunner runner = new AnimationRunner(component);
|
||||
|
||||
double full = 1D;
|
||||
double emphasized = 1.2D;
|
||||
Double[] stages = new Double[] { full, emphasized, emphasized, emphasized, full };
|
||||
runner.setValues(stages);
|
||||
runner.setDuration(Duration.ofMillis(500));
|
||||
runner.setRemovePainterWhenFinished(false); // we will remove it ourselves
|
||||
runner.setPainter(painter);
|
||||
runner.start();
|
||||
|
||||
animationRunner = runner;
|
||||
|
||||
// remove the text later so users have a chance to read it
|
||||
timerMonitor = GTimer.scheduleRunnable(hideDelay.toMillis(), () -> {
|
||||
Swing.runNow(() -> hide());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides any message being displayed. This can be called even if the message has been hidden.
|
||||
*/
|
||||
public void hide() {
|
||||
|
||||
if (animationRunner != null) {
|
||||
animationRunner.stop();
|
||||
animationRunner = null;
|
||||
}
|
||||
|
||||
if (timerMonitor != null) {
|
||||
timerMonitor.cancel();
|
||||
timerMonitor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Json.toString(message);
|
||||
}
|
||||
|
||||
//=================================================================================================
|
||||
// Inner Classes
|
||||
//=================================================================================================
|
||||
|
||||
private abstract class AbstractTextPainer implements AnimationPainter {
|
||||
|
||||
private static String FONT_ID = "font.glasspane.message";
|
||||
private static final String MESSAGE_FG_COLOR_ID = "color.fg.glasspane.message";
|
||||
|
||||
// use an image of the painted text to make scaling smoother; cache the image for speed
|
||||
protected Image baseTextImage;
|
||||
|
||||
private ComponentListener resizeListener = new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
baseTextImage = null;
|
||||
Window w = WindowUtilities.windowForComponent(component);
|
||||
w.repaint();
|
||||
}
|
||||
};
|
||||
|
||||
AbstractTextPainer() {
|
||||
component.addComponentListener(resizeListener);
|
||||
}
|
||||
|
||||
private void createImage() {
|
||||
|
||||
if (baseTextImage != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Font font = Gui.getFont(FONT_ID);
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
|
||||
scratchG2d.setFont(font);
|
||||
Dimension size = getComponentSize();
|
||||
int padding = 20;
|
||||
size.width -= padding;
|
||||
TextShaper textShaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
Dimension textSize = textShaper.getTextSize();
|
||||
if (textSize.width == 0 || textSize.height == 0) {
|
||||
return; // not enough room to paint text
|
||||
}
|
||||
|
||||
// Add some space to handle float to int rounding in the text calculation. This prevents
|
||||
// the edge of characters from getting clipped when painting.
|
||||
int roundingPadding = 5;
|
||||
int w = textSize.width + roundingPadding;
|
||||
int h = textSize.height;
|
||||
|
||||
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = (Graphics2D) bi.getGraphics();
|
||||
g2d.setFont(font);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
g2d.setColor(Gui.getColor(MESSAGE_FG_COLOR_ID));
|
||||
|
||||
textShaper.drawText(g2d);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
baseTextImage = bi;
|
||||
}
|
||||
|
||||
protected Dimension getComponentSize() {
|
||||
Rectangle r = component.getVisibleRect();
|
||||
Dimension size = r.getSize();
|
||||
Container parent = component.getParent();
|
||||
if (parent instanceof JScrollPane || parent instanceof JViewport) {
|
||||
// this handles covering the component when it is inside of a scroll pane
|
||||
size = parent.getSize();
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
protected Rectangle getComponentBounds(GGlassPane glassPane) {
|
||||
|
||||
Rectangle r = component.getVisibleRect();
|
||||
Point point = r.getLocation();
|
||||
Dimension size = r.getSize();
|
||||
|
||||
Container parent = component.getParent();
|
||||
Component coordinateSource = parent;
|
||||
if (parent instanceof JScrollPane || parent instanceof JViewport) {
|
||||
// this handles covering the component when it is inside of a scroll pane
|
||||
point = parent.getLocation();
|
||||
size = parent.getSize();
|
||||
coordinateSource = parent.getParent();
|
||||
}
|
||||
|
||||
point = SwingUtilities.convertPoint(coordinateSource, point, glassPane);
|
||||
return new Rectangle(point, size);
|
||||
}
|
||||
|
||||
protected Image updateImage(Graphics2D g2d, double scale) {
|
||||
|
||||
baseTextImage = null;
|
||||
|
||||
createImage();
|
||||
|
||||
if (baseTextImage == null) {
|
||||
return null; // this implies an exception happened
|
||||
}
|
||||
|
||||
int w = baseTextImage.getWidth(null);
|
||||
int h = baseTextImage.getHeight(null);
|
||||
|
||||
int sw = ((int) (w * scale));
|
||||
int sh = ((int) (h * scale));
|
||||
|
||||
int iw = baseTextImage.getWidth(null);
|
||||
int ih = baseTextImage.getHeight(null);
|
||||
|
||||
if (iw == sw && ih == sh) {
|
||||
return baseTextImage; // nothing to change
|
||||
}
|
||||
|
||||
return ImageUtils.createScaledImage(baseTextImage, sw, sh, 0);
|
||||
}
|
||||
|
||||
protected void paintOverComponent(Graphics2D g2d, GGlassPane glassPane) {
|
||||
|
||||
Rectangle bounds = getComponentBounds(glassPane);
|
||||
|
||||
float alpha = .7F; // arbitrary; allow some of the background to be visible
|
||||
AlphaComposite alphaComposite = AlphaComposite
|
||||
.getInstance(AlphaComposite.SrcOver.getRule(), alpha);
|
||||
Composite originalComposite = g2d.getComposite();
|
||||
Color originalColor = g2d.getColor();
|
||||
g2d.setComposite(alphaComposite);
|
||||
g2d.setColor(component.getBackground());
|
||||
|
||||
g2d.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
|
||||
|
||||
g2d.setComposite(originalComposite);
|
||||
g2d.setColor(originalColor);
|
||||
}
|
||||
}
|
||||
|
||||
private class CenterTextPainter extends AbstractTextPainer {
|
||||
|
||||
@Override
|
||||
public void paint(GGlassPane glassPane, Graphics graphics, double intensity) {
|
||||
|
||||
Graphics2D g2d = (Graphics2D) graphics;
|
||||
|
||||
Image image = updateImage(g2d, intensity);
|
||||
if (image == null) {
|
||||
return; // this implies an exception happened
|
||||
}
|
||||
|
||||
// use visible rectangle to get the correct size when in a scroll pane
|
||||
Rectangle componentBounds = getComponentBounds(glassPane);
|
||||
|
||||
// without room to draw the message, skip so we don't draw over other components
|
||||
int imageHeight = image.getHeight(null);
|
||||
if (imageHeight > componentBounds.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
paintOverComponent(g2d, glassPane);
|
||||
|
||||
// note: textHeight and textWidth will vary depending on the intensity
|
||||
int textHeight = image.getHeight(null);
|
||||
int textWidth = image.getWidth(null);
|
||||
int padding = 5;
|
||||
int middleY = componentBounds.y + (componentBounds.height / 2);
|
||||
int middleX = componentBounds.x + (componentBounds.width / 2);
|
||||
int requiredHeight = textHeight + padding;
|
||||
int requiredWidth = textWidth + padding;
|
||||
int y = middleY - (requiredHeight / 2);
|
||||
int x = middleX - (requiredWidth / 2);
|
||||
|
||||
g2d.drawImage(image, x, y, null);
|
||||
|
||||
// debug
|
||||
// g2d.setColor(Palette.BLUE);
|
||||
// g2d.drawRect(x, y, textWidth, textHeight);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class BottomTextPainter extends AbstractTextPainer {
|
||||
|
||||
@Override
|
||||
public void paint(GGlassPane glassPane, Graphics graphics, double intensity) {
|
||||
|
||||
Graphics2D g2d = (Graphics2D) graphics;
|
||||
|
||||
Image image = updateImage(g2d, intensity);
|
||||
if (image == null) {
|
||||
return; // this implies an exception happened
|
||||
}
|
||||
|
||||
// use visible rectangle to get the correct size when in a scroll pane
|
||||
Rectangle componentBounds = getComponentBounds(glassPane);
|
||||
|
||||
// without room to draw the message, skip so we don't draw over other components
|
||||
int imageHeight = image.getHeight(null);
|
||||
if (imageHeight > componentBounds.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
paintOverComponent(g2d, glassPane);
|
||||
|
||||
int textHeight = image.getHeight(null);
|
||||
int padding = 5;
|
||||
int bottom = componentBounds.y + componentBounds.height;
|
||||
int requiredHeight = textHeight + padding;
|
||||
int y = bottom - requiredHeight;
|
||||
int x = componentBounds.x + padding;
|
||||
|
||||
g2d.drawImage(image, x, y, null);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 docking.util;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.font.*;
|
||||
import java.text.AttributedCharacterIterator;
|
||||
import java.text.AttributedString;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A class that will layout text into lines based on the given display size. This class requires
|
||||
* the graphics context in order to correctly size the text.
|
||||
*/
|
||||
public class TextShaper {
|
||||
|
||||
private List<TextShaperLine> lines = new ArrayList<>();
|
||||
private Dimension textSize = new Dimension(0, 0);
|
||||
|
||||
private String originalText;
|
||||
private String clippedText;
|
||||
private Dimension displaySize = new Dimension(0, 0);
|
||||
private Graphics2D g2d;
|
||||
|
||||
/**
|
||||
* Creates a text shaper with the given text, display size and graphics context.
|
||||
* @param text the text
|
||||
* @param displaySize the size
|
||||
* @param g2d the graphics
|
||||
*/
|
||||
public TextShaper(String text, Dimension displaySize, Graphics2D g2d) {
|
||||
this.originalText = text;
|
||||
this.clippedText = text;
|
||||
this.displaySize = displaySize;
|
||||
this.g2d = g2d;
|
||||
|
||||
// Trim blank lines we don't want
|
||||
// Drop all blank lines before and after the non-blank lines. It seems pointless to paint
|
||||
// these blank lines. We can change this if there is a valid reason to do so.
|
||||
text = removeNewlinesAroundText(text);
|
||||
text = text.replaceAll("\t", " ");
|
||||
|
||||
init(text);
|
||||
}
|
||||
|
||||
private String removeNewlinesAroundText(String s) {
|
||||
int first = 0;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c != '\n') {
|
||||
first = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
s = s.substring(first);
|
||||
|
||||
int last = s.length() - 1;
|
||||
for (int i = last; i >= 0; i--) {
|
||||
char c = s.charAt(i);
|
||||
if (c != '\n') {
|
||||
last = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return s.substring(0, last + 1);
|
||||
}
|
||||
|
||||
private void init(String currentText) {
|
||||
|
||||
if (displaySize.width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create the attributed string needed by the LineBreakMeasurer, setting the font over all
|
||||
// of the text
|
||||
AttributedString as = new AttributedString(currentText);
|
||||
int length = currentText.length();
|
||||
Font font = g2d.getFont();
|
||||
as.addAttribute(TextAttribute.FONT, font, 0, length);
|
||||
|
||||
// create the LineBreakMeasuerer we will use to split the text at the given width, using the
|
||||
// rendering environment to get accurate size information
|
||||
AttributedCharacterIterator paragraph = as.getIterator();
|
||||
FontRenderContext frc = g2d.getFontRenderContext();
|
||||
LineBreakMeasurer measurer = new LineBreakMeasurer(paragraph, frc);
|
||||
measurer.setPosition(paragraph.getBeginIndex());
|
||||
|
||||
int totalHeight = 0;
|
||||
int largestWidth = 0;
|
||||
int position = 0;
|
||||
while ((position = measurer.getPosition()) < paragraph.getEndIndex()) {
|
||||
|
||||
TextShaperLine line = createLine(currentText, measurer);
|
||||
|
||||
// Look ahead to see if the new row we created will fit within the height restrictions.
|
||||
// If not, we must clip the text and do this work again.
|
||||
float rowHeight = line.getHeight();
|
||||
totalHeight += rowHeight;
|
||||
if (totalHeight > displaySize.height) {
|
||||
|
||||
// Truncate the original text and try again with the smaller text that we now know
|
||||
// will fit, adding an ellipsis.
|
||||
int lineCount = lines.size();
|
||||
lines.clear();
|
||||
|
||||
if (lineCount == 0) {
|
||||
return; // no room for a single line of text
|
||||
}
|
||||
|
||||
// clip the text of the and recalculate
|
||||
int end = position;
|
||||
int newEnd = end - 3; // 3 for '...'
|
||||
clippedText = currentText.substring(0, newEnd) + "...";
|
||||
|
||||
init(clippedText);
|
||||
return;
|
||||
}
|
||||
|
||||
lines.add(line);
|
||||
|
||||
largestWidth = Math.max(largestWidth, (int) line.getWidth());
|
||||
|
||||
}
|
||||
|
||||
textSize = new Dimension(largestWidth, totalHeight);
|
||||
}
|
||||
|
||||
private TextShaperLine createLine(String currentText, LineBreakMeasurer measurer) {
|
||||
|
||||
// nextOffset() finds the end of the text that fits into the max width
|
||||
int position = measurer.getPosition();
|
||||
int wrappingWidth = displaySize.width;
|
||||
int nextEnd = measurer.nextOffset(wrappingWidth);
|
||||
|
||||
// special case: look for newlines in the current line and split the text on that
|
||||
// newline instead so that user-requested newlines are painted
|
||||
int limit = updateLimitForNewline(currentText, position, nextEnd);
|
||||
|
||||
TextShaperLine line = null;
|
||||
if (limit == 0) {
|
||||
// A limit of 0 implies the first character of the text is a newline. Add a full blank
|
||||
// line to handle that case. This can happen with consecutive newlines or if a line
|
||||
// happened to break with a leading newline.
|
||||
Font font = g2d.getFont();
|
||||
FontRenderContext frc = g2d.getFontRenderContext();
|
||||
LineMetrics lm = font.getLineMetrics("W", frc);
|
||||
line = new BlankLine(lm.getHeight());
|
||||
|
||||
// advance the measurer to move past the single newline
|
||||
measurer.nextLayout(wrappingWidth, position + 1, false);
|
||||
}
|
||||
else {
|
||||
// create a layout with the given limit (either restricted by width or by a newline)
|
||||
TextLayout layout = measurer.nextLayout(wrappingWidth, position + limit, false);
|
||||
int nextPosition = measurer.getPosition();
|
||||
String lineText = currentText.substring(position, nextPosition);
|
||||
line = new TextLayoutLine(lineText, layout);
|
||||
}
|
||||
|
||||
// If we limited the current line to break on the newline, then move past that newline so it
|
||||
// is not in the next line we process. Since we have broken the line already, we do not
|
||||
// need that newline character.
|
||||
movePastTrailingNewline(currentText, measurer);
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
private int updateLimitForNewline(String text, int position, int limit) {
|
||||
int newline = text.indexOf('\n', position);
|
||||
if (newline != -1) {
|
||||
if (newline >= position && newline < limit) {
|
||||
// newline will be in the current line; break on the newline
|
||||
return newline - position;
|
||||
}
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
private void movePastTrailingNewline(String text, LineBreakMeasurer measurer) {
|
||||
int newPosition = measurer.getPosition();
|
||||
if (newPosition < text.length()) {
|
||||
char nextChar = text.charAt(newPosition);
|
||||
if (nextChar == '\n') {
|
||||
measurer.setPosition(newPosition + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bounds of the wrapped text of this class
|
||||
* @return the bounds of the wrapped text of this class
|
||||
*/
|
||||
public Dimension getTextSize() {
|
||||
return textSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the text is too large to fit in the original display size
|
||||
* @return true if the text is too large to fit in the original display size
|
||||
*/
|
||||
public boolean isClipped() {
|
||||
return !Objects.equals(originalText, clippedText);
|
||||
}
|
||||
|
||||
public List<TextShaperLine> getLines() {
|
||||
return Collections.unmodifiableList(lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the wrapped text into the graphics used to create this class.
|
||||
* @param g the graphics into which the text should be painted.
|
||||
*/
|
||||
public void drawText(Graphics2D g) {
|
||||
float dy = 0;
|
||||
for (TextShaperLine line : lines) {
|
||||
float y = dy + line.getAscent(); // move the drawing down to the start of the next line
|
||||
line.draw(g, 0, y);
|
||||
dy += line.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TextShaperLine {
|
||||
abstract float getHeight();
|
||||
|
||||
abstract float getWidth();
|
||||
|
||||
abstract float getAscent();
|
||||
|
||||
abstract String getText();
|
||||
|
||||
abstract boolean isBlank();
|
||||
|
||||
abstract void draw(Graphics2D g, float x, float y);
|
||||
}
|
||||
|
||||
private class TextLayoutLine extends TextShaperLine {
|
||||
private String lineText;
|
||||
private TextLayout layout;
|
||||
|
||||
TextLayoutLine(String text, TextLayout layout) {
|
||||
this.lineText = text;
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getAscent() {
|
||||
return layout.getAscent();
|
||||
}
|
||||
|
||||
@Override
|
||||
float getHeight() {
|
||||
return (int) (layout.getAscent() + layout.getDescent() + layout.getLeading());
|
||||
}
|
||||
|
||||
@Override
|
||||
float getWidth() {
|
||||
return (float) layout.getBounds().getWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
String getText() {
|
||||
return lineText;
|
||||
}
|
||||
|
||||
@Override
|
||||
void draw(Graphics2D g, float x, float y) {
|
||||
layout.draw(g, x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isBlank() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return lineText;
|
||||
}
|
||||
}
|
||||
|
||||
private class BlankLine extends TextShaperLine {
|
||||
|
||||
private float lineHeight;
|
||||
|
||||
BlankLine(float lineHeight) {
|
||||
this.lineHeight = lineHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getAscent() {
|
||||
return 0; // the value shouldn't matter, since we don't actually draw anything
|
||||
}
|
||||
|
||||
@Override
|
||||
float getHeight() {
|
||||
return lineHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getWidth() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isBlank() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String getText() {
|
||||
return "\n";
|
||||
}
|
||||
|
||||
@Override
|
||||
void draw(Graphics2D g, float x, float y) {
|
||||
// nothing to draw
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Blank Line";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 docking.util;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import docking.DockingFrame;
|
||||
import docking.test.AbstractDockingTest;
|
||||
import docking.util.TextShaper.TextShaperLine;
|
||||
import generic.theme.Gui;
|
||||
import ghidra.util.bean.GGlassPane;
|
||||
|
||||
public class TextShaperTest extends AbstractDockingTest {
|
||||
|
||||
// @Test
|
||||
// for debugging
|
||||
public void testShowMessage() {
|
||||
|
||||
JLabel label = new JLabel("<html>This is<br>some text that<br>spans multiple lines.");
|
||||
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.add(label);
|
||||
|
||||
DockingFrame frame = new DockingFrame("Test Frame");
|
||||
frame.getContentPane().add(panel);
|
||||
GGlassPane glassPane = new GGlassPane();
|
||||
frame.setGlassPane(glassPane);
|
||||
frame.setSize(400, 400);
|
||||
frame.setVisible(true);
|
||||
|
||||
GGlassPaneMessage glassPaneMessage = new GGlassPaneMessage(label);
|
||||
glassPaneMessage
|
||||
.showCenteredMessage(
|
||||
"This is a test and (newline\n\nhere) some more text to reach the width limit. " +
|
||||
"More text to (tab here\t\t) to come as we type.");
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShaper() {
|
||||
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
Font font = Gui.getFont("font.monospaced").deriveFont(24);
|
||||
scratchG2d.setFont(font);
|
||||
|
||||
Dimension size = new Dimension(1000, 100);
|
||||
String message = "This is a message";
|
||||
TextShaper shaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
List<TextShaperLine> lines = shaper.getLines();
|
||||
assertEquals(1, lines.size());
|
||||
assertFalse(shaper.isClipped());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShaper_LineWrap() {
|
||||
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
Font font = Gui.getFont("font.monospaced").deriveFont(24);
|
||||
scratchG2d.setFont(font);
|
||||
|
||||
Dimension size = new Dimension(100, 100);
|
||||
String message = "This is a long message";
|
||||
TextShaper shaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
List<TextShaperLine> lines = shaper.getLines();
|
||||
assertEquals(2, lines.size());
|
||||
assertFalse(shaper.isClipped());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShaper_NewLine() {
|
||||
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
Font font = Gui.getFont("font.monospaced").deriveFont(24);
|
||||
scratchG2d.setFont(font);
|
||||
|
||||
Dimension size = new Dimension(1000, 100);
|
||||
String message = "This is a long\nmessage";
|
||||
TextShaper shaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
List<TextShaperLine> lines = shaper.getLines();
|
||||
assertEquals(2, lines.size());
|
||||
assertEquals("This is a long", lines.get(0).getText());
|
||||
assertEquals("message", lines.get(1).getText());
|
||||
assertFalse(shaper.isClipped());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShaper_NewLines_Consecutive() {
|
||||
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
Font font = Gui.getFont("font.monospaced").deriveFont(24);
|
||||
scratchG2d.setFont(font);
|
||||
|
||||
Dimension size = new Dimension(1000, 100);
|
||||
String message = "This is a long\n\nmessage";
|
||||
TextShaper shaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
List<TextShaperLine> lines = shaper.getLines();
|
||||
assertEquals(3, lines.size());
|
||||
assertEquals("This is a long", lines.get(0).getText());
|
||||
assertEquals("\n", lines.get(1).getText());
|
||||
assertEquals("message", lines.get(2).getText());
|
||||
assertFalse(shaper.isClipped());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShaper_NewLines_AroundText() {
|
||||
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
Font font = Gui.getFont("font.monospaced").deriveFont(24);
|
||||
scratchG2d.setFont(font);
|
||||
|
||||
Dimension size = new Dimension(1000, 100);
|
||||
String message = "\n\nThis is a long message\n\n";
|
||||
TextShaper shaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
List<TextShaperLine> lines = shaper.getLines();
|
||||
assertEquals(1, lines.size());
|
||||
assertFalse(shaper.isClipped());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShaper_Tabs() {
|
||||
|
||||
BufferedImage tempImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D scratchG2d = (Graphics2D) tempImage.getGraphics();
|
||||
Font font = Gui.getFont("font.monospaced").deriveFont(24);
|
||||
scratchG2d.setFont(font);
|
||||
|
||||
Dimension size = new Dimension(1000, 100);
|
||||
String message = "This is a\t\tmessage";
|
||||
TextShaper shaper = new TextShaper(message, size, scratchG2d);
|
||||
|
||||
List<TextShaperLine> lines = shaper.getLines();
|
||||
assertEquals(1, lines.size());
|
||||
assertEquals("This is a message", lines.get(0).getText());
|
||||
assertFalse(shaper.isClipped());
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
* REVIEWED: YES
|
||||
*
|
||||
* 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.
|
||||
@ -16,51 +15,25 @@
|
||||
*/
|
||||
package generic.timer;
|
||||
|
||||
import ghidra.util.SystemUtilities;
|
||||
import java.util.Timer;
|
||||
|
||||
import ghidra.util.SystemUtilities;
|
||||
import ghidra.util.timer.GTimer;
|
||||
|
||||
/**
|
||||
* Creates a new {@link GhidraTimer} appropriate for a headed or headless environment.
|
||||
* <P>
|
||||
* If running a headed environment, the callback will happen on the Swing thread. Otherwise, the
|
||||
* callback will happen on the non-Swing {@link Timer} thread.
|
||||
* <P>
|
||||
* See also {@link GTimer}
|
||||
*/
|
||||
public class GhidraTimerFactory {
|
||||
public static GhidraTimer getGhidraTimer(int initialDelay, int delay, TimerCallback callback) {
|
||||
if (SystemUtilities.isInHeadlessMode()) {
|
||||
return new GhidraSwinglessTimer(initialDelay, delay, callback);
|
||||
|
||||
|
||||
}
|
||||
return new GhidraSwingTimer(initialDelay, delay, callback);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
System.setProperty(SystemUtilities.HEADLESS_PROPERTY, "true");
|
||||
final GhidraTimer t = GhidraTimerFactory.getGhidraTimer(500,500,null);
|
||||
t.setDelay(500);
|
||||
TimerCallback callback1 = new TimerCallback() {
|
||||
int i = 0;
|
||||
public void timerFired() {
|
||||
System.out.println("A: "+i);
|
||||
if (++i == 20) {
|
||||
t.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setTimerCallback(callback1);
|
||||
t.start();
|
||||
|
||||
final GhidraTimer t2 = GhidraTimerFactory.getGhidraTimer(250, 1000, null);
|
||||
|
||||
TimerCallback callback2 = new TimerCallback() {
|
||||
int i = 0;
|
||||
public void timerFired() {
|
||||
System.out.println("B: "+i);
|
||||
if (++i == 100) {
|
||||
t2.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
t2.setInitialDelay(250);
|
||||
t2.setTimerCallback(callback2);
|
||||
t2.start();
|
||||
|
||||
|
||||
|
||||
Thread.sleep(20000);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@ -18,12 +18,17 @@ package ghidra.util.timer;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import generic.timer.GhidraTimerFactory;
|
||||
import ghidra.util.Msg;
|
||||
|
||||
/**
|
||||
* A class to schedule {@link Runnable}s to run after some delay, optionally repeating. This class
|
||||
* uses a {@link Timer} internally to schedule work. Clients of this class are given a monitor
|
||||
* that allows them to check on the state of the runnable, as well as to cancel the runnable.
|
||||
* <P>
|
||||
* Note: The callback will be called on the {@link Timer}'s thread.
|
||||
* <P>
|
||||
* See also {@link GhidraTimerFactory}
|
||||
*/
|
||||
public class GTimer {
|
||||
private static Timer timer;
|
||||
|
@ -4,9 +4,9 @@
|
||||
* 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.
|
||||
@ -69,6 +69,7 @@ public class GGlassPane extends JComponent {
|
||||
* @param painter the painter to add
|
||||
*/
|
||||
public void addPainter(GGlassPanePainter painter) {
|
||||
painters.remove(painter);
|
||||
painters.add(painter);
|
||||
repaint();
|
||||
}
|
||||
@ -99,6 +100,7 @@ public class GGlassPane extends JComponent {
|
||||
|
||||
/**
|
||||
* Sets the busy state of all glass panes created in the VM.
|
||||
* @param isBusy the busy state of all glass panes created in the VM.
|
||||
*/
|
||||
public static void setAllGlassPanesBusy(boolean isBusy) {
|
||||
for (GGlassPane glassPane : systemGlassPanes) {
|
||||
@ -108,6 +110,7 @@ public class GGlassPane extends JComponent {
|
||||
|
||||
/**
|
||||
* Returns true if this glass pane is blocking user input.
|
||||
* @return true if this glass pane is blocking user input.
|
||||
*/
|
||||
public boolean isBusy() {
|
||||
return isBusy;
|
||||
|
Loading…
Reference in New Issue
Block a user