diff --git a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java index 4cf79e60df..fb2e9d8894 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/features/base/memsearch/gui/MemorySearchProvider.java @@ -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); + } + } diff --git a/Ghidra/Framework/Docking/data/docking.theme.properties b/Ghidra/Framework/Docking/data/docking.theme.properties index ddaa0432c9..6ae7b334bd 100644 --- a/Ghidra/Framework/Docking/data/docking.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.theme.properties @@ -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 diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationPainter.java b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationPainter.java index 27bb0f6af0..24043506e4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationPainter.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationPainter.java @@ -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); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationRunner.java b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationRunner.java new file mode 100644 index 0000000000..47b7864245 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationRunner.java @@ -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. + *
+ * A {@link #setPainter(AnimationPainter) painter} must be supplied before calling {@link #start()}. + * A simple example usage: + *
+ * 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(); + * } + * + *+ *
+ * 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. + *
+ * 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. + *
+ * 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); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java index 879865a2ce..5f6b8883be 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java @@ -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() { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/GGlassPaneMessage.java b/Ghidra/Framework/Docking/src/main/java/docking/util/GGlassPaneMessage.java new file mode 100644 index 0000000000..482926e365 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/GGlassPaneMessage.java @@ -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. + *
+ * 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);
+
+ }
+
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/TextShaper.java b/Ghidra/Framework/Docking/src/main/java/docking/util/TextShaper.java
new file mode 100644
index 0000000000..d041dd563e
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/util/TextShaper.java
@@ -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
+ * 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.
+ *
+ * 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);
-
- }
}
diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
index bb80759137..7a5b9eccdf 100644
--- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/timer/GTimer.java
@@ -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.
+ *
+ * Note: The callback will be called on the {@link Timer}'s thread.
+ *
+ * See also {@link GhidraTimerFactory}
*/
public class GTimer {
private static Timer timer;
diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/bean/GGlassPane.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/bean/GGlassPane.java
index 8b67bf48c1..cb117b955d 100644
--- a/Ghidra/Framework/Gui/src/main/java/ghidra/util/bean/GGlassPane.java
+++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/bean/GGlassPane.java
@@ -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;
some text that
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