diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java index e85c94a283..077da0aaf3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/analysis/AutoAnalysisManager.java @@ -1513,12 +1513,6 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl return Math.max(dominantMonitor.getProgress(), slaveMonitor.getProgress()); } - @Override - public void reportIssue(Issue issue) { - dominantMonitor.reportIssue(issue); - slaveMonitor.reportIssue(issue); - } - @Override public void cancel() { dominantMonitor.cancel(); @@ -1535,16 +1529,6 @@ public class AutoAnalysisManager implements DomainObjectListener, DomainObjectCl dominantMonitor.addCancelledListener(listener); } - @Override - public void addIssueListener(IssueListener listener) { - dominantMonitor.addIssueListener(listener); - } - - @Override - public void removeIssueListener(IssueListener listener) { - dominantMonitor.removeIssueListener(listener); - } - @Override public void setCancelEnabled(boolean enable) { dominantMonitor.setCancelEnabled(enable); diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessTimedTaskMonitor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessTimedTaskMonitor.java index f9f9b2dbf9..1026fec578 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessTimedTaskMonitor.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/headless/HeadlessTimedTaskMonitor.java @@ -18,9 +18,9 @@ package ghidra.app.util.headless; import java.util.Timer; import java.util.TimerTask; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; -import ghidra.util.task.*; +import ghidra.util.task.CancelledListener; +import ghidra.util.task.TaskMonitor; /** * Monitor used by Headless Analyzer for "timeout" functionality @@ -99,11 +99,6 @@ public class HeadlessTimedTaskMonitor implements TaskMonitor { return 0; } - @Override - public void reportIssue(Issue issue) { - // stub - } - @Override public void cancel() { timer.cancel(); // Terminate the timer thread @@ -120,16 +115,6 @@ public class HeadlessTimedTaskMonitor implements TaskMonitor { // stub } - @Override - public void addIssueListener(IssueListener listener) { - // stub - } - - @Override - public void removeIssueListener(IssueListener listener) { - // stub - } - @Override public void setCancelEnabled(boolean enable) { // stub diff --git a/Ghidra/Features/Base/src/test/java/ghidra/util/task/TaskMonitorServiceTest.java b/Ghidra/Features/Base/src/test/java/ghidra/util/task/TaskMonitorServiceTest.java new file mode 100644 index 0000000000..4e70cb7407 --- /dev/null +++ b/Ghidra/Features/Base/src/test/java/ghidra/util/task/TaskMonitorServiceTest.java @@ -0,0 +1,341 @@ +/* ### + * 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 ghidra.util.task; + +import static org.junit.Assert.*; + +import java.util.concurrent.atomic.*; + +import org.junit.*; + +import ghidra.test.AbstractGhidraHeadedIntegrationTest; +import ghidra.util.SystemUtilities; +import ghidra.util.exception.CancelledException; + +/** + * Tests for the {@link TaskMonitorService} + */ +public class TaskMonitorServiceTest extends AbstractGhidraHeadedIntegrationTest { + + @Before + public void setup() { + // nothing to do + } + + @After + public void teardown() { + // nothing to do + } + + /** + * Verifies that in a single-threaded environment, the {@link TaskMonitorService} + * returns the correct instance of {@link TaskMonitor}. In this case, the initial + * monitor is registered by the {@link TaskLauncher task launcher}; the next call + * to {@link TaskMonitorService#getMonitor() getMonitor} should return the same. + */ + @Test + public void testSingleThread() { + + TaskLauncher.launch(new Task("task1") { + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + TaskMonitor newMonitor = TaskMonitorService.getMonitor(); + assertSame(newMonitor, monitor); + } + + }); + + waitForTasks(); + } + + /** + * Verifies that in a single-threaded environment, the {@link TaskMonitorService} + * returns the same instance of {@link TaskMonitor} each time one is + * requested. + *

+ * Note that the first time a monitor is requested it will always return the primary + * monitor that allows progress changes. Each subsequent time it will return the + * secondary monitor; these secondary monitors is what this test is verifying. + */ + @Test + public void testSingleThreadSecondaryMonitors() { + + TaskLauncher.launch(new Task("task1") { + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + // First monitor requested is always the primary - just make the call so + // we can get to retrieving some secondary monitors + TaskMonitorService.getMonitor(); + + TaskMonitor secondaryMonitor1 = TaskMonitorService.getMonitor(); + TaskMonitor secondaryMonitor2 = TaskMonitorService.getMonitor(); + + assertEquals(secondaryMonitor1, secondaryMonitor2); + } + + }); + + waitForTasks(); + } + + /** + * Verifies that in a multi-threaded environment, the {@link TaskMonitorService} + * returns a different instance of {@link TaskMonitor} for each thread. + */ + @Test + public void testMultipleThreads() { + + AtomicReference localMonitor1 = new AtomicReference<>(); + AtomicReference localMonitor2 = new AtomicReference<>(); + + TaskLauncher.launch(new Task("task1") { + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + localMonitor1.set(TaskMonitorService.getMonitor()); + } + + }); + + TaskLauncher.launch(new Task("task2") { + + @Override + public void run(TaskMonitor monitor) throws CancelledException { + localMonitor2.set(TaskMonitorService.getMonitor()); + } + + }); + + waitForTasks(); + + assertNotSame(localMonitor1.get(), localMonitor2.get()); + } + + /** + * Tests that if we try to register a monitor while on the Swing thread an exception will be + * thrown. + */ + @Test + public void testSetMonitorOnSwingThread() { + + SystemUtilities.runSwingNow(() -> { + + try { + TaskMonitorService.register(TaskMonitor.DUMMY); + } + catch (Exception e) { + // expected catch + return; + } + + fail("Successful register of monitor on Swing thread (should not have been allowed)"); + }); + + waitForTasks(); + + } + + /** + * Verifies that if a client attempts to set a monitor on a thread that already has a monitor, + * an exception will be thrown. + *

+ * Note: The first monitor is registered behind the scenes by the task launcher + */ + @Test + public void testRegisterMultipleMonitors() { + + TaskLauncher.launch(new Task("task") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + try { + TaskMonitorService.register(TaskMonitor.DUMMY); + } + catch (Exception ex) { + // expected catch + return; + } + + fail("Successful register of new monitor (should not have been allowed)"); + } + }); + + waitForTasks(); + } + + /** + * Verifies that unique monitor id's are correctly assigned to each thread that registers + * a monitor + */ + @Test + public void testMonitorIds() { + + AtomicInteger monitor1Id = new AtomicInteger(); + AtomicInteger monitor2Id = new AtomicInteger(); + AtomicInteger monitor3Id = new AtomicInteger(); + + Thread thread1 = + new Thread(() -> monitor1Id.set(TaskMonitorService.register(TaskMonitor.DUMMY))); + + Thread thread2 = + new Thread(() -> monitor2Id.set(TaskMonitorService.register(TaskMonitor.DUMMY))); + + Thread thread3 = + new Thread(() -> monitor3Id.set(TaskMonitorService.register(TaskMonitor.DUMMY))); + + thread1.start(); + thread2.start(); + thread3.start(); + + waitForTasks(); + + boolean areIdsDifferent = + (monitor1Id.get() != monitor2Id.get()) && (monitor2Id.get() != monitor3Id.get()); + assertTrue(areIdsDifferent); + } + + /** + * Verifies that if a client attempts to remove a monitor with an invalid id the request + * will fail + */ + @Test + public void testRemoveMonitorFail() { + + AtomicBoolean removed = new AtomicBoolean(true); + + // All monitor id's are positive integers; this is guaranteed to generated a failure + final int BOGUS_ID = -1; + + Thread thread1 = new Thread(() -> { + TaskMonitorService.register(TaskMonitor.DUMMY); + try { + TaskMonitorService.remove(BOGUS_ID); + } + catch (Exception e) { + // expected catch + removed.set(false); + } + }); + + thread1.start(); + + waitForTasks(); + + assertFalse(removed.get()); + } + + /** + * Verifies that a monitor can be successfully removed given a correct id + */ + @Test + public void testRemoveMonitorSuccess() { + + AtomicBoolean removed = new AtomicBoolean(false); + + AtomicInteger monitorId = new AtomicInteger(); + + Thread thread1 = new Thread(() -> { + monitorId.set(TaskMonitorService.register(TaskMonitor.DUMMY)); + + try { + TaskMonitorService.remove(monitorId.get()); + removed.set(true); + } + catch (Exception e) { + // should not be here + fail(); + } + }); + + thread1.start(); + + waitForTasks(); + + assertTrue(removed.get()); + } + + /** + * Verifies that the first monitor returned from the service will be the + * primary monitor + */ + @Test + public void testRetrievePrimaryMonitor() { + + TaskLauncher.launch(new Task("task") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + TaskMonitor monitor1 = TaskMonitorService.getMonitor(); + assertTrue(monitor1 instanceof TaskDialog); + } + }); + + waitForTasks(); + } + + /** + * Verifies that any subsequent requests after the initial request will result in + * a {@link SecondaryTaskDialog} being returned + */ + @Test + public void testRetrieveSecondaryMonitor() { + + TaskLauncher.launch(new Task("task") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + TaskMonitor monitor1 = TaskMonitorService.getMonitor(); + assertTrue(monitor1 instanceof TaskDialog); + + monitor1 = TaskMonitorService.getMonitor(); + assertTrue(monitor1 instanceof SecondaryTaskMonitor); + } + }); + + waitForTasks(); + } + + /** + * Verifies that once a monitor has been reset, the next call to retrieve a monitor will + * return it as a primary + */ + @Test + public void testMonitorReset() { + + TaskLauncher.launch(new Task("task") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + TaskMonitor monitor1 = TaskMonitorService.getMonitor(); + assertTrue(monitor1 instanceof TaskDialog); + + monitor1 = TaskMonitorService.getMonitor(); + assertTrue(monitor1 instanceof SecondaryTaskMonitor); + + monitor1.reset(); + + monitor1 = TaskMonitorService.getMonitor(); + assertTrue(monitor1 instanceof TaskDialog); + } + }); + + waitForTasks(); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java index da56f6ec0e..cbd2399f02 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/DialogComponentProvider.java @@ -67,6 +67,7 @@ public class DialogComponentProvider protected JButton dismissButton; private boolean isAlerting; private JLabel statusLabel; + private JLabel subStatusLabel; private JPanel statusProgPanel; // contains status panel and progress panel private Timer showTimer; private TaskScheduler taskScheduler; @@ -608,6 +609,11 @@ public class DialogComponentProvider public void setStatusText(String text) { setStatusText(text, MessageType.INFO); } + + @Override + public void setSubStatusText(String text) { + setSubStatusText(text, MessageType.INFO); + } /** * Sets the text in the dialog's status line using the specified message type to control @@ -621,12 +627,24 @@ public class DialogComponentProvider setStatusText(message, type, false); } + @Override + public void setSubStatusText(String message, MessageType type) { + setSubStatusText(message, type, false); + } + @Override public void setStatusText(String message, MessageType type, boolean alert) { String text = StringUtils.isBlank(message) ? " " : message; SystemUtilities.runIfSwingOrPostSwingLater(() -> doSetStatusText(text, type, alert)); } + + @Override + public void setSubStatusText(String message, MessageType type, boolean alert) { + + String text = StringUtils.isBlank(message) ? " " : message; + SystemUtilities.runIfSwingOrPostSwingLater(() -> doSetSubStatusText(text, type, alert)); + } private void doSetStatusText(String text, MessageType type, boolean alert) { @@ -641,6 +659,20 @@ public class DialogComponentProvider alertMessage(); } } + + private void doSetSubStatusText(String text, MessageType type, boolean alert) { + + SystemUtilities.assertThisIsTheSwingThread( + "Setting text must be performed on the Swing thread"); + + subStatusLabel.setText(text); + subStatusLabel.setForeground(getStatusColor(type)); + updateStatusToolTip(); + + if (alert) { + alertMessage(); + } + } /** * Signals for this dialog to visually draw the user's attention to the status text @@ -679,11 +711,13 @@ public class DialogComponentProvider // normal Swing mechanism may not have yet happened). mainPanel.validate(); statusLabel.setVisible(false); // disable painting in this dialog so we don't see double + subStatusLabel.setVisible(false); Animator animator = AnimationUtils.pulseComponent(statusLabel, 1); animator.addTarget(new TimingTargetAdapter() { @Override public void end() { statusLabel.setVisible(true); + subStatusLabel.setVisible(true); alertFinishedCallback.call(); isAlerting = false; } @@ -755,12 +789,7 @@ public class DialogComponentProvider private void showProgressBar(String localTitle, boolean hasProgress, boolean canCancel) { taskMonitorComponent.setTaskName(localTitle); taskMonitorComponent.showProgress(hasProgress); - if (canCancel) { - taskMonitorComponent.showCancelButton(true); - } - else { - taskMonitorComponent.showCancelButton(false); - } + taskMonitorComponent.setCancelButtonVisibility(canCancel); progressCardLayout.show(statusProgPanel, PROGRESS); rootPanel.validate(); } @@ -799,18 +828,29 @@ public class DialogComponentProvider public void clearStatusText() { SystemUtilities.runIfSwingOrPostSwingLater(() -> { statusLabel.setText(" "); + subStatusLabel.setText(" "); updateStatusToolTip(); }); } /** - * returns the current status in the dialogs status line= + * Returns the current status in the dialogs status line + * * @return the status text */ public String getStatusText() { return statusLabel.getText(); } + /** + * Returns the secondary status message + * + * @return the secondary status message + */ + public String getSubStatusText() { + return subStatusLabel.getText(); + } + protected JLabel getStatusLabel() { return statusLabel; } @@ -901,6 +941,14 @@ public class DialogComponentProvider updateStatusToolTip(); } }); + + subStatusLabel = new JLabel(); + subStatusLabel.setName("subStatusLabel"); + subStatusLabel.setHorizontalAlignment(SwingConstants.CENTER); + subStatusLabel.setForeground(Color.blue); + subStatusLabel.setFont(subStatusLabel.getFont().deriveFont(Font.ITALIC)); + subStatusLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + subStatusLabel.setFont(subStatusLabel.getFont().deriveFont(11.0f)); // use a strut panel so the size of the message area does not change if we make // the message label not visible @@ -908,6 +956,7 @@ public class DialogComponentProvider panel.add(Box.createVerticalStrut(height), BorderLayout.WEST); panel.add(statusLabel, BorderLayout.CENTER); + panel.add(subStatusLabel, BorderLayout.SOUTH); return panel; } diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/ChompingBitsAnimationPanel.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/ChompingBitsAnimationPanel.java new file mode 100644 index 0000000000..e37b9af018 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/ChompingBitsAnimationPanel.java @@ -0,0 +1,48 @@ +/* ### + * 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 ghidra.util.task; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; + +import docking.util.AnimatedIcon; +import resources.ResourceManager; + +/** + * Panel that displays an animation of the Ghidra dragon chomping bits. + */ +public class ChompingBitsAnimationPanel extends JPanel { + + public ChompingBitsAnimationPanel() { + setLayout(new BorderLayout()); + + List iconList = new ArrayList<>(); + iconList.add(ResourceManager.loadImage("images/eatbits1.png")); + iconList.add(ResourceManager.loadImage("images/eatbits2.png")); + iconList.add(ResourceManager.loadImage("images/eatbits3.png")); + iconList.add(ResourceManager.loadImage("images/eatbits4.png")); + iconList.add(ResourceManager.loadImage("images/eatbits5.png")); + iconList.add(ResourceManager.loadImage("images/eatbits6.png")); + iconList.add(ResourceManager.loadImage("images/eatbits7.png")); + AnimatedIcon icon = new AnimatedIcon(iconList, 200, 0); + setSize(new Dimension(200, 100)); + add(new JLabel(icon)); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/HourglassAnimationPanel.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/HourglassAnimationPanel.java new file mode 100644 index 0000000000..f7d15aac1a --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/HourglassAnimationPanel.java @@ -0,0 +1,59 @@ +/* ### + * 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 ghidra.util.task; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; + +import docking.util.AnimatedIcon; +import resources.ResourceManager; + +/** + * Panel that displays an animation of a spinning hourglass + */ +public class HourglassAnimationPanel extends JPanel { + + public HourglassAnimationPanel() { + + setLayout(new BorderLayout()); + + List iconList = new ArrayList<>(); + iconList.add(ResourceManager.loadImage("images/hourglass24_01.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_02.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_02.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_03.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_03.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_04.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_04.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_05.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_05.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_06.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_06.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_07.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_07.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_08.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_08.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_09.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_10.png")); + iconList.add(ResourceManager.loadImage("images/hourglass24_11.png")); + AnimatedIcon progressIcon = new AnimatedIcon(iconList, 150, 0); + + add (new JLabel(progressIcon), BorderLayout.CENTER); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/RunManager.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/RunManager.java index 922ae71ce7..76c91f00eb 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/RunManager.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/RunManager.java @@ -245,7 +245,7 @@ public class RunManager { * @param showCancel true means to show the cancel button */ public void showCancelButton(boolean showCancel) { - monitor.showCancelButton(showCancel); + monitor.setCancelButtonVisibility(showCancel); } /** diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/SecondaryTaskMonitor.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/SecondaryTaskMonitor.java new file mode 100644 index 0000000000..44f0df423e --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/SecondaryTaskMonitor.java @@ -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 ghidra.util.task; + +import java.util.Objects; + +import ghidra.util.exception.CancelledException; + +/** + * {@link TaskMonitor} that restricts users from being able to update the progress bar. The class + * is initialized with another, fully-featured monitor and forwards all requests to it, + * but squashes calls to methods that are not allowed. + *

+ * Note: Two instances of this class are deemed equal if they have the same {@link #parentMonitor}, + * hence the override of {@link #hashCode()} and {@link #equals(Object)}. + */ +public class SecondaryTaskMonitor implements TaskMonitor { + + private TaskMonitor parentMonitor; + + /** + * Constructor + * + * @param parentMonitor the fully-functional task monitor this is based off of + */ + public SecondaryTaskMonitor(TaskMonitor parentMonitor) { + this.parentMonitor = parentMonitor; + } + + /** + * Overridden to ensure that clients who have this type of monitor will only update the + * secondary message when using this method + * + * @param message the message string to display + */ + @Override + public void setMessage(String message) { + if (parentMonitor instanceof TaskDialog) { + ((TaskDialog) parentMonitor).setSecondaryMessage(message); + } + parentMonitor.setMessage(message); + } + + @Override + public void setCancelEnabled(boolean enable) { + parentMonitor.setCancelEnabled(enable); + } + + @Override + public void setInitialized(boolean init) { + parentMonitor.setInitialized(init); + } + + @Override + public boolean isCancelEnabled() { + return parentMonitor.isCancelEnabled(); + } + + @Override + public boolean isCancelled() { + return parentMonitor.isCancelled(); + } + + @Override + public synchronized void cancel() { + parentMonitor.cancel(); + } + + @Override + public synchronized void clearCanceled() { + parentMonitor.clearCanceled(); + } + + @Override + public void checkCanceled() throws CancelledException { + parentMonitor.checkCanceled(); + } + + @Override + public void addCancelledListener(CancelledListener listener) { + parentMonitor.addCancelledListener(listener); + } + + @Override + public void removeCancelledListener(CancelledListener listener) { + parentMonitor.removeCancelledListener(listener); + } + + @Override + public long getMaximum() { + return parentMonitor.getMaximum(); + } + + @Override + public long getProgress() { + return parentMonitor.getProgress(); + } + + @Override + public void setShowProgressValue(boolean showProgressValue) { + // squash + } + + @Override + public void setProgress(long value) { + // squash + } + + @Override + public void initialize(long max) { + // squash + } + + @Override + public void setMaximum(long max) { + // squash + } + + @Override + public void setIndeterminate(boolean indeterminate) { + // squash + } + + @Override + public void incrementProgress(long incrementAmount) { + // squash + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((parentMonitor == null) ? 0 : parentMonitor.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SecondaryTaskMonitor other = (SecondaryTaskMonitor) obj; + if (!Objects.equals(parentMonitor, other.parentMonitor)) { + return false; + } + + return true; + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskDialog.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskDialog.java index f251765ac8..836ca37e8d 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskDialog.java @@ -15,20 +15,20 @@ */ package ghidra.util.task; -import java.awt.*; +import java.awt.BorderLayout; +import java.awt.Component; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.swing.*; import docking.DialogComponentProvider; import docking.DockingWindowManager; -import docking.util.AnimatedIcon; import docking.widgets.OptionDialog; -import docking.widgets.label.GIconLabel; -import ghidra.util.*; +import ghidra.util.HelpLocation; +import ghidra.util.SystemUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.timer.GTimer; -import resources.ResourceManager; /** * Dialog that is displayed to show activity for a Task that is running outside of the @@ -36,25 +36,48 @@ import resources.ResourceManager; */ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { + /** Timer used to give the task a chance to complete */ private static final int SLEEPY_TIME = 10; + + /** Amount of time to wait before showing the monitor dialog */ private final static int MAX_DELAY = 200000; + public final static int DEFAULT_WIDTH = 275; private Timer showTimer; - private TaskMonitorComponent monitorComponent; private AtomicInteger taskID = new AtomicInteger(); - private boolean canCancel; - private Runnable updateMessage; private Runnable closeDialog; - private Runnable enableCancelButton; - private String newMessage; - private boolean cancelState = true; private Component centerOnComp; private Runnable shouldCancelRunnable; - private boolean done; + private boolean taskDone; private JPanel mainPanel; + private ChompingBitsAnimationPanel chompingBitsPanel; + private TaskMonitorComponent monitorComponent; - /** Creates new TaskDialog + /** Runnable that updates the primary message label in the dialog */ + private Runnable updatePrimaryMessageRunnable; + + /** Runnable that updates the secondary message label in the dialog */ + private Runnable updateSecondaryMessageRunnable; + + /** If not null, then the value of the string has yet to be rendered */ + private String newPrimaryMessage; + + /** If not null, then the value of the string has yet to be rendered */ + private String newSecondaryMessage; + + /** + * Indicates if this monitor has been initialized for progress updates. If this value + * is set to true, the {@link TaskMonitorService} will not return the monitor to + * another caller (only one client should be able to update progress at a time). + */ + private AtomicBoolean initialized = new AtomicBoolean(false); + + private SecondaryTaskMonitor secondaryTaskMonitor; + + /** + * Constructor + * * @param centerOnComp component to be centered over when shown, * otherwise center over parent. If both centerOnComp and parent * are null, dialog will be centered on screen. @@ -65,7 +88,9 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { task.hasProgress()); } - /** Creates a new TaskDialog. + /** + * Constructor + * * @param task the Task that this dialog will be associated with */ public TaskDialog(Task task) { @@ -73,18 +98,20 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { } /** - * Construct new TaskDialog. + * Constructor + * * @param title title for the dialog * @param canCancel true if the task can be canceled * @param isModal true if the dialog should be modal * @param hasProgress true if the dialog should show a progress bar */ public TaskDialog(String title, boolean canCancel, boolean isModal, boolean hasProgress) { - this(null, title, isModal, canCancel, true /*hasProgress*/); + this(null, title, isModal, canCancel, hasProgress); } - + /** - * Construct new TaskDialog. + * Constructor + * * @param centerOnComp component to be centered over when shown, otherwise center over * parent. If both centerOnComp is null, then the active window will be used * @param title title for the dialog @@ -99,8 +126,21 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { setup(canCancel, hasProgress); } + @Override + public boolean isInitialized() { + return initialized.get(); + } + + @Override + public void setInitialized(boolean init) { + this.initialized.set(init); + } + private void setup(boolean canCancel, boolean hasProgress) { - this.canCancel = canCancel; + monitorComponent = new TaskMonitorComponent(false, false); + chompingBitsPanel = new ChompingBitsAnimationPanel(); + + setCancelEnabled(canCancel); setRememberLocation(false); setRememberSize(false); setTransient(true); @@ -108,13 +148,18 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { close(); dispose(); }; - updateMessage = () -> { - setStatusText(newMessage); + updatePrimaryMessageRunnable = () -> { + setStatusText(newPrimaryMessage); synchronized (TaskDialog.this) { - newMessage = null; + newPrimaryMessage = null; + } + }; + updateSecondaryMessageRunnable = () -> { + setSubStatusText(newSecondaryMessage); + synchronized (TaskDialog.this) { + newSecondaryMessage = null; } }; - enableCancelButton = () -> TaskDialog.super.setCancelEnabled(cancelState); shouldCancelRunnable = () -> { int currentTaskID = taskID.get(); @@ -124,9 +169,9 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { } }; - monitorComponent = new TaskMonitorComponent(false, false); mainPanel = new JPanel(new BorderLayout()); addWorkPanel(mainPanel); + if (hasProgress) { installProgressMonitor(); } @@ -142,6 +187,11 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { setHelpLocation(new HelpLocation("Tool", "TaskDialog")); } + /** + * Shows a dialog asking the user if they really, really want to cancel the task + * + * @return true if the task should be cancelled + */ protected boolean promptToVerifyCancel() { boolean userSaysYes = OptionDialog.showYesNoDialog(getComponent(), "Cancel?", "Do you really want to cancel \"" + getTitle() + "\"?") == OptionDialog.OPTION_ONE; @@ -150,34 +200,24 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { } /** - * Creates the main work panel for the dialog + * Adds the panel that contains the progress bar to the dialog */ private void installProgressMonitor() { SystemUtilities.runIfSwingOrPostSwingLater(() -> { mainPanel.removeAll(); - - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(20, 10, 5, 10)); - panel.add(monitorComponent); - mainPanel.add(panel, BorderLayout.NORTH); - + mainPanel.add(monitorComponent, BorderLayout.CENTER); repack(); }); } + /** + * Adds the panel that contains the chomping bits animation to the dialog. This should only be + * called if the dialog has no need to display progress. + */ private void installActivityDisplay() { SystemUtilities.runIfSwingOrPostSwingLater(() -> { mainPanel.removeAll(); - - JPanel panel = new JPanel(new BorderLayout()); - panel.setSize(new Dimension(200, 100)); - String[] filenames = { "images/eatbits1.png", "images/eatbits2.png", - "images/eatbits3.png", "images/eatbits4.png", "images/eatbits5.png", - "images/eatbits6.png", "images/eatbits7.png" }; - panel.add( - new GIconLabel(new AnimatedIcon(ResourceManager.loadImages(filenames), 200, 0))); - mainPanel.add(panel, BorderLayout.CENTER); - + mainPanel.add(chompingBitsPanel, BorderLayout.CENTER); repack(); }); } @@ -200,7 +240,9 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { monitorComponent.setShowProgressValue(showProgressValue); } - /** Sets the percentage done. + /** + * Sets the percentage done + * * @param param The percentage of the task completed. */ @Override @@ -250,62 +292,58 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { monitorComponent.setIndeterminate(indeterminate); } - /** Called if the user presses the cancel button on - * the dialog - */ @Override protected void cancelCallback() { - synchronized (this) { - if (!monitorComponent.isCancelEnabled() || monitorComponent.isCancelled()) { - return; - } - } - SwingUtilities.invokeLater(shouldCancelRunnable); } - /** Sets the message in the TaskDialog dialog - * @param str The message string to be displayed - */ @Override synchronized public void setMessage(String str) { - boolean invoke = (newMessage == null); - newMessage = str; + boolean invoke = (newPrimaryMessage == null); if (invoke) { - SwingUtilities.invokeLater(updateMessage); + newPrimaryMessage = str; + SwingUtilities.invokeLater(updatePrimaryMessageRunnable); } } /** - * Set the enable state of the Cancel button. - * @param enable the state to set the cancel button. + * Updates the secondary message on the task monitor + * + * @param str the string to update */ - @Override - public void setCancelEnabled(boolean enable) { - if (canCancel) { - monitorComponent.setCancelEnabled(enable); - SwingUtilities.invokeLater(enableCancelButton); + synchronized public void setSecondaryMessage(String str) { + boolean invoke = (newSecondaryMessage == null); + if (invoke) { + newSecondaryMessage = str; + SwingUtilities.invokeLater(updateSecondaryMessageRunnable); } } + @Override + public void setCancelEnabled(boolean enable) { + monitorComponent.setCancelEnabled(enable); + super.setCancelEnabled(enable); + } + @Override public boolean isCancelEnabled() { - return canCancel && cancelState; + return super.isCancelEnabled(); } public synchronized void taskProcessed() { - done = true; + taskDone = true; monitorComponent.notifyChangeListeners(); SwingUtilities.invokeLater(closeDialog); } + @Override public synchronized void reset() { - done = false; + taskDone = false; taskID.incrementAndGet(); } public synchronized boolean isCompleted() { - return done; + return taskDone; } @Override @@ -393,7 +431,7 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { @Override public synchronized void cancel() { - if (!canCancel || monitorComponent.isCancelled()) { + if (monitorComponent.isCancelled()) { return; } // Mark as cancelled, must be detected by task which should terminate @@ -432,18 +470,11 @@ public class TaskDialog extends DialogComponentProvider implements TaskMonitor { } @Override - public void addIssueListener(IssueListener listener) { - monitorComponent.addIssueListener(listener); + public synchronized TaskMonitor getSecondaryMonitor() { + if (secondaryTaskMonitor == null) { + secondaryTaskMonitor = new SecondaryTaskMonitor(this); + } + return secondaryTaskMonitor; } - @Override - public void removeIssueListener(IssueListener listener) { - monitorComponent.removeIssueListener(listener); - - } - - @Override - public void reportIssue(Issue issue) { - monitorComponent.reportIssue(issue); - } } diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java index f132759d8e..4aba7f8b8b 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/task/TaskMonitorComponent.java @@ -24,75 +24,79 @@ import java.util.concurrent.atomic.AtomicInteger; import javax.swing.*; import docking.ToolTipManager; -import docking.util.AnimatedIcon; import docking.widgets.EmptyBorderButton; import docking.widgets.OptionDialog; import docking.widgets.label.GDHtmlLabel; -import docking.widgets.label.GIconLabel; -import ghidra.util.Issue; import ghidra.util.SystemUtilities; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; import ghidra.util.exception.CancelledException; import ghidra.util.layout.VerticalLayout; import resources.Icons; -import resources.ResourceManager; /** * Component that contains a progress bar, a progress icon, and a cancel * button to cancel the task that is associated with this task monitor. + *

* By default the progress bar and progress icon (spinning globe) are visible. */ public class TaskMonitorComponent extends JPanel implements TaskMonitor { private WeakSet listeners = WeakDataStructureFactory.createCopyOnReadWeakSet(); - private WeakSet issueListeners; - private JButton cancelButton; - private JPanel eastButtonPanel; + private JProgressBar progressBar; + private JButton cancelButton; + + private JPanel cancelPanel; + private JPanel progressBarPanel; + private JPanel mainContentPanel; private JPanel progressPanel; - private JPanel activeProgressPanel; - private JLabel imageLabel; + private String progressMessage; private String taskName; + + private JLabel messageLabel; + private volatile boolean isCancelled; - private String message; - + private long lastProgress = -1; private long progress; - private long lastMax = -1; - private long max; + private long lastMaxProgress = -1; + private long maxProgress; + private long scaleFactor = 1; private Runnable updateProgressPanelRunnable; private Runnable updateCancelButtonRunnable; private Runnable updateToolTipRunnable; - private JLabel messageLabel; + private Runnable shouldCancelRunnable; private boolean showingProgress = true; private boolean showingIcon = true; private boolean showingCancelButton = true; private boolean cancelEnabled = true; + private boolean paintProgressValue = true; + private AtomicBoolean isIndeterminate = new AtomicBoolean(false); private AtomicInteger taskID = new AtomicInteger(); private Timer updateTimer; - private Runnable shouldCancelRunnable; - - private boolean paintProgressValue = true; + private NumberFormat percentFormat = NumberFormat.getPercentInstance(); - private long scaleFactor = 1; - + /** - * Construct a new TaskMonitorComponent. - * @param l listener that is notified when the task completes or the - * user cancels the task + * Constructor */ - public TaskMonitorComponent() { this(true, true); } + /** + * Constructor + * + * @param includeTextField if true, the dialog can display a status progressMessage with progress details + * @param includeCancelButton if true, a cancel button will be displayed + */ public TaskMonitorComponent(boolean includeTextField, boolean includeCancelButton) { updateProgressPanelRunnable = () -> updateProgressPanel(); updateCancelButtonRunnable = () -> updateCancelButton(); @@ -113,13 +117,25 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { buildProgressPanel(includeTextField, includeCancelButton); } + + @Override + public void addCancelledListener(CancelledListener mcl) { + listeners.add(mcl); + } - /** - * Reset this monitor so that it can be reused. - */ - public synchronized void reset() { - isCancelled = false; - taskID.incrementAndGet(); + @Override + public void removeCancelledListener(CancelledListener mcl) { + listeners.remove(mcl); + } + + @Override + public void incrementProgress(long incrementAmount) { + setProgress(progress + incrementAmount); + } + + @Override + public long getProgress() { + return progress; } @Override @@ -136,7 +152,7 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { @Override public synchronized void setMessage(String message) { - this.message = message; + this.progressMessage = message; startUpdateTimer(); } @@ -148,13 +164,7 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { progress = value; startUpdateTimer(); } - - private synchronized void startUpdateTimer() { - if (!updateTimer.isRunning()) { - updateTimer.start(); - } - } - + @Override public void initialize(long maxValue) { setMaximum(maxValue); @@ -163,13 +173,13 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { @Override public void setMaximum(long max) { - this.max = max; - if (progress > this.max) { + this.maxProgress = max; + if (progress > this.maxProgress) { progress = max; } startUpdateTimer(); } - + /** * Sets the indeterminate property of the progress bar, * which determines whether the progress bar is in determinate @@ -201,17 +211,6 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { }); } - /** - * Returns true if {@link #setIndeterminate(boolean)} with a value of true has - * been called. - * - * @return true if {@link #setIndeterminate(boolean)} with a value of true has - * been called. - */ - public boolean isIndeterminate() { - return isIndeterminate.get(); - } - @Override public synchronized void setCancelEnabled(boolean enable) { if (cancelEnabled != enable) { @@ -243,67 +242,7 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { isCancelled = false; } } - - /** - * Set whether the progress bar should be visible. - * @param b true if the progress bar should be visible - */ - public synchronized void showProgress(boolean b) { - if (b != showingProgress) { - showingProgress = b; - SystemUtilities.runSwingLater(updateProgressPanelRunnable); - } - } - - /** - * Set the name of the task; the name shows up in the tool tip for - * the cancel button. - * @param name the name of the task - */ - public void setTaskName(String name) { - taskName = name; - SystemUtilities.runSwingLater(updateToolTipRunnable); - } - - /** - * Show or not show the cancel button according to the showCancel param. - */ - public void showCancelButton(boolean showCancel) { - - if (showCancel == showingCancelButton) { - return; - } - - if (showCancel) { - add(eastButtonPanel, BorderLayout.EAST); - } - else { - remove(eastButtonPanel); - } - showingCancelButton = showCancel; - } - - /** - * Show or not show the progress icon (spinning globe) according to - * the showIcon param. - */ - public void showProgressIcon(final boolean showIcon) { - if (showIcon == showingIcon) { - return; - } - Runnable r = () -> { - if (showIcon) { - activeProgressPanel.add(imageLabel, BorderLayout.EAST); - } - else { - activeProgressPanel.remove(imageLabel); - } - showingIcon = showIcon; - }; - - SystemUtilities.runSwingNow(r); - } - + @Override public void setShowProgressValue(boolean showProgressValue) { this.paintProgressValue = showProgressValue; @@ -312,19 +251,121 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { @Override public long getMaximum() { - return max; + return maxProgress; + } + + /** + * Reset this monitor so that it can be reused + */ + @Override + public synchronized void reset() { + isCancelled = false; + taskID.incrementAndGet(); + } + + /** + * Returns true if {@link #setIndeterminate(boolean)} with a value of true has + * been called. + * + * @return true if {@link #setIndeterminate(boolean)} with a value of true has + * been called. + */ + public boolean isIndeterminate() { + return isIndeterminate.get(); + } + + /** + * Set whether the progress bar should be visible + * + * @param show true if the progress bar should be visible + */ + public synchronized void showProgress(boolean show) { + if (show != showingProgress) { + showingProgress = show; + SystemUtilities.runSwingLater(updateProgressPanelRunnable); + } + } + + /** + * Set the name of the task; the name shows up in the tool tip for + * the cancel button. + * + * @param name the name of the task + */ + public void setTaskName(String name) { + taskName = name; + SystemUtilities.runSwingLater(updateToolTipRunnable); + } + + /** + * Set the visibility of the cancel button + * + * @param visible if true, show the cancel button; false otherwise + */ + public void setCancelButtonVisibility(boolean visible) { + + if (visible == showingCancelButton) { + return; + } + + if (visible) { + add(cancelPanel, BorderLayout.EAST); + } + else { + remove(cancelPanel); + } + + repaint(); + showingCancelButton = visible; + } + + /** + * Sets the visibility of the progress icon + * + * @param visible if true, display the progress icon + */ + public void showProgressIcon(boolean visible) { + if (visible == showingIcon) { + return; + } + Runnable r = () -> { + if (visible) { + mainContentPanel.add(progressPanel, BorderLayout.EAST); + } + else { + mainContentPanel.remove(progressPanel); + } + showingIcon = visible; + }; + + SystemUtilities.runSwingNow(r); + } + + protected void notifyChangeListeners() { + Runnable r = () -> { + for (CancelledListener mcl : listeners) { + mcl.cancelled(); + } + }; + SwingUtilities.invokeLater(r); + } + + private synchronized void startUpdateTimer() { + if (!updateTimer.isRunning()) { + updateTimer.start(); + } } private synchronized void update() { - if (message != null) { - messageLabel.setText(message); - message = null; + if (progressMessage != null) { + messageLabel.setText(progressMessage); + progressMessage = null; } - if (max != lastMax) { - setMaxValueInProgressBar(max); - lastMax = max; + if (maxProgress != lastMaxProgress) { + setMaxValueInProgressBar(maxProgress); + lastMaxProgress = maxProgress; } if (progress != lastProgress) { @@ -379,10 +420,10 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { private synchronized void updateProgressPanel() { if (showingProgress) { - progressPanel.add(progressBar, BorderLayout.NORTH); + progressBarPanel.add(progressBar, BorderLayout.NORTH); } else { - progressPanel.remove(progressBar); + progressBarPanel.remove(progressBar); } } @@ -424,24 +465,24 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { progressBar.setStringPainted(true); ToolTipManager.sharedInstance().registerComponent(progressBar); - createAnimatedIcon(); + progressPanel = new HourglassAnimationPanel(); - progressPanel = new JPanel(new VerticalLayout(0)); - progressPanel.add(progressBar); + progressBarPanel = new JPanel(new VerticalLayout(0)); + progressBarPanel.add(progressBar); if (includeTextField) { - progressPanel.add(messageLabel); + progressBarPanel.add(messageLabel); progressBar.setPreferredSize(new Dimension(180, 12)); } else { progressBar.setBorderPainted(true); Dimension size = progressBar.getPreferredSize(); - progressPanel.setBorder(BorderFactory.createEmptyBorder( - (imageLabel.getPreferredSize().height - size.height) / 2, 0, 0, 8)); + progressBarPanel.setBorder(BorderFactory.createEmptyBorder( + (progressPanel.getPreferredSize().height - size.height) / 2, 0, 0, 8)); } - activeProgressPanel = new JPanel(new BorderLayout()); - activeProgressPanel.add(progressPanel, BorderLayout.CENTER); - activeProgressPanel.add(imageLabel, BorderLayout.EAST); + mainContentPanel = new JPanel(new BorderLayout()); + mainContentPanel.add(progressBarPanel, BorderLayout.CENTER); + mainContentPanel.add(progressPanel, BorderLayout.EAST); ImageIcon icon = Icons.STOP_ICON; cancelButton = new EmptyBorderButton(icon); @@ -452,86 +493,15 @@ public class TaskMonitorComponent extends JPanel implements TaskMonitor { cancelButton.setFocusable(false); cancelButton.setRolloverEnabled(true); - add(activeProgressPanel, BorderLayout.CENTER); + add(mainContentPanel, BorderLayout.CENTER); if (includeCancelButton) { - eastButtonPanel = new JPanel(); - eastButtonPanel.setLayout(new BoxLayout(eastButtonPanel, BoxLayout.Y_AXIS)); - eastButtonPanel.add(Box.createVerticalGlue()); - eastButtonPanel.add(cancelButton); - eastButtonPanel.add(Box.createVerticalGlue()); - add(eastButtonPanel, BorderLayout.EAST); - } - } - - private void createAnimatedIcon() { - String[] filenames = { "images/hourglass24_01.png", "images/hourglass24_02.png", - "images/hourglass24_02.png", "images/hourglass24_03.png", "images/hourglass24_03.png", - "images/hourglass24_04.png", "images/hourglass24_04.png", "images/hourglass24_05.png", - "images/hourglass24_05.png", "images/hourglass24_06.png", "images/hourglass24_06.png", - "images/hourglass24_07.png", "images/hourglass24_07.png", "images/hourglass24_08.png", - "images/hourglass24_08.png", "images/hourglass24_09.png", "images/hourglass24_10.png", - "images/hourglass24_11.png" }; - imageLabel = - new GIconLabel(new AnimatedIcon(ResourceManager.loadImages(filenames), 150, 0)); - } - - @Override - public void incrementProgress(long incrementAmount) { - setProgress(progress + incrementAmount); - } - - @Override - public long getProgress() { - return progress; - } - - protected void notifyChangeListeners() { - Runnable r = () -> { - synchronized (listeners) { - for (CancelledListener mcl : listeners) { - mcl.cancelled(); - } - } - }; - SwingUtilities.invokeLater(r); - } - - @Override - public void addCancelledListener(CancelledListener mcl) { - synchronized (listeners) { - listeners.add(mcl); - } - } - - @Override - public void removeCancelledListener(CancelledListener mcl) { - synchronized (listeners) { - listeners.remove(mcl); - } - } - - @Override - public void addIssueListener(IssueListener listener) { - if (issueListeners == null) { - issueListeners = WeakDataStructureFactory.createCopyOnWriteWeakSet(); - } - } - - @Override - public void removeIssueListener(IssueListener listener) { - if (issueListeners != null) { - issueListeners.remove(listener); - } - - } - - @Override - public void reportIssue(Issue issue) { - if (issueListeners != null) { - for (IssueListener listener : issueListeners) { - listener.issueReported(issue); - } + cancelPanel = new JPanel(); + cancelPanel.setLayout(new BoxLayout(cancelPanel, BoxLayout.Y_AXIS)); + cancelPanel.add(Box.createVerticalGlue()); + cancelPanel.add(cancelButton); + cancelPanel.add(Box.createVerticalGlue()); + add(cancelPanel, BorderLayout.EAST); } } } diff --git a/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskDialogTest.java b/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskDialogTest.java index 08df0dc777..bc1ce42fd1 100644 --- a/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskDialogTest.java +++ b/Ghidra/Framework/Docking/src/test/java/ghidra/util/task/TaskDialogTest.java @@ -28,6 +28,7 @@ import org.junit.After; import org.junit.Test; import docking.test.AbstractDockingTest; +import ghidra.util.exception.CancelledException; public class TaskDialogTest extends AbstractDockingTest { @@ -94,6 +95,89 @@ public class TaskDialogTest extends AbstractDockingTest { assertTrue(dialogSpy.wasShown()); assertSwingThreadFinishedBeforeTask(); } + + /* + * Verifies that if the dialog cancel button is activated, the task is cancelled + */ + @Test + public void testTaskCancel() throws Exception { + SlowModalTask task = new SlowModalTask(); + TaskDialogSpy dialogSpy = launchTask(task); + + dialogSpy.doShow(); + + waitForTask(); + + assertFalse(dialogSpy.isCancelled()); + dialogSpy.cancel(); + assertTrue(dialogSpy.isCancelled()); + } + + /* + * Verifies that if the task does not allow cancellation, the cancel button on the GUI + * is disabled + */ + @Test + public void testTaskNoCancel() throws Exception { + SlowModalTask task = new SlowModalTask(); + TaskDialogSpy dialogSpy = launchTask(task); + + dialogSpy.doShow(); + dialogSpy.setCancelEnabled(false); + + waitForTask(); + + assertFalse(dialogSpy.isCancelEnabled()); + } + + /* + * Verifies that the progress value can be successfully updated + * after using the {@link TaskMonitorService} to retrieve a monitor. + */ + @Test + public void testUpdateProgressSuccess() throws Exception { + + TaskLauncher.launch(new Task("task") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + TaskMonitor monitor1 = TaskMonitorService.getMonitor(); + long val = monitor1.getProgress(); + + monitor1.setProgress(10); + val = monitor1.getProgress(); + assertEquals(val, 10); + } + }); + } + + /* + * Verifies that the progress value will NOT be updated if the caller is a + * secondary monitor. As a bonus, this also verifies that the Task Launcher does + * not lock the task for future progress updates when a new task is launched. + */ + @Test + public void testUpdatePogressFail() throws Exception { + + TaskLauncher.launch(new Task("task") { + @Override + public void run(TaskMonitor monitor) throws CancelledException { + + TaskMonitor monitor1 = TaskMonitorService.getMonitor(); + TaskMonitor monitor2 = TaskMonitorService.getMonitor(); + + // Update should be accepted + monitor1.setProgress(10); + + // Update should fail + monitor2.setProgress(20); + + long val = monitor2.getProgress(); + assertEquals(val, 10); + } + }); + + waitForTasks(); + } private void assertSwingThreadBlockedForTask() { TDEvent lastEvent = eventQueue.peekLast(); @@ -160,7 +244,7 @@ public class TaskDialogTest extends AbstractDockingTest { public TaskDialogSpy(Task task) { super(task); } - + @Override protected void doShow() { shown.set(true); diff --git a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/FutureTaskMonitor.java b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/FutureTaskMonitor.java index ee941222f3..143da1390d 100644 --- a/Ghidra/Framework/Generic/src/main/java/generic/concurrent/FutureTaskMonitor.java +++ b/Ghidra/Framework/Generic/src/main/java/generic/concurrent/FutureTaskMonitor.java @@ -18,9 +18,9 @@ package generic.concurrent; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; -import ghidra.util.task.*; +import ghidra.util.task.CancelledListener; +import ghidra.util.task.TaskMonitor; /** * This is the FutureTask that will be used to call the {@link QCallback} to work on @@ -131,11 +131,6 @@ class FutureTaskMonitor extends FutureTask implements TaskMonitor { return currentProgress; } - @Override - public void reportIssue(Issue issue) { - // TODO - } - @Override public boolean cancel(boolean mayInterruptIfRunning) { boolean result = super.cancel(mayInterruptIfRunning); @@ -191,16 +186,6 @@ class FutureTaskMonitor extends FutureTask implements TaskMonitor { } } - @Override - public void addIssueListener(IssueListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeIssueListener(IssueListener listener) { - throw new UnsupportedOperationException(); - } - private static class ChainedCancelledListener implements CancelledListener { private volatile CancelledListener listener1; private volatile CancelledListener listener2; diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StatusListener.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StatusListener.java index 367f847401..d0a5efc903 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/StatusListener.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/StatusListener.java @@ -41,6 +41,30 @@ public interface StatusListener { * @param alert true to grab the user's attention */ void setStatusText(String text, MessageType type, boolean alert); + + /** + * Sets the subtask text + * + * @param text the text to set + */ + void setSubStatusText(String text); + + /** + * Sets the subtask text + * + * @param text the text to set + * @param type the message type + */ + void setSubStatusText(String text, MessageType type); + + /** + * Sets the subtask text + * + * @param text the text to set + * @param type the message type + * @param alert if true, an alert will be generated + */ + void setSubStatusText(String text, MessageType type, boolean alert); /** * Clear the current status - same as setStatusText("") diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/CancelOnlyWrappingTaskMonitor.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/CancelOnlyWrappingTaskMonitor.java index 40eec77abe..3a7fdb30cb 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/CancelOnlyWrappingTaskMonitor.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/CancelOnlyWrappingTaskMonitor.java @@ -15,8 +15,6 @@ */ package ghidra.util.task; -import ghidra.util.Issue; - /** * A monitor that is designed for sub-tasks, where the outer task handles reporting messages and * progress. This class is really just for checking cancelled. @@ -88,11 +86,6 @@ public class CancelOnlyWrappingTaskMonitor extends WrappingTaskMonitor { return 0; } - @Override - public void reportIssue(Issue issue) { - // ignore - } - @Override public void setCancelEnabled(boolean enable) { // ignore diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/Task.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/Task.java index 305da42309..0fe9266f73 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/Task.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/Task.java @@ -112,14 +112,19 @@ public abstract class Task implements MonitoredRunnable { /** * When an object implementing interface Runnable is used to create a thread, * starting the thread causes the object's run method to be called in that - * separately executing thread + * separately executing thread. + *

+ * Note that the monitor is handed to the {@link TaskMonitorService} here so it will be + * available to any users request it via {@link TaskMonitorService#getMonitor() getMonitor}. * - * @param monitor The TaskMonitor + * @param monitor the task monitor */ @Override public final void monitoredRun(TaskMonitor monitor) { this.taskMonitor = monitor; + int monitorId = TaskMonitorService.register(monitor); + // this will be removed from SystemUtilities in Task.run() after the task is finished TaskUtilities.addTrackedTask(this, monitor); @@ -136,9 +141,14 @@ public abstract class Task implements MonitoredRunnable { getTaskTitle() + " - Uncaught Exception: " + t.toString(), t); } finally { - // this is put into SystemUtilities by the TaskLauncher + + // This should not be necessary since the thread local object will be cleaned up + // by the GC when the thread terminates, but it's here in case we ever use this + // in conjunction with thread pools and have to manually remove them. + TaskMonitorService.remove(monitorId); + TaskUtilities.removeTrackedTask(this); - this.taskMonitor = null; + this.taskMonitor = null; } notifyTaskListeners(isCancelled); diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorAdapter.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorAdapter.java index 31781489d7..2a68b53d0e 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorAdapter.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorAdapter.java @@ -15,7 +15,6 @@ */ package ghidra.util.task; -import ghidra.util.Issue; import ghidra.util.datastruct.WeakDataStructureFactory; import ghidra.util.datastruct.WeakSet; import ghidra.util.exception.CancelledException; @@ -163,19 +162,4 @@ public class TaskMonitorAdapter implements TaskMonitor { public synchronized void removeCancelledListener(CancelledListener listener) { listeners.remove(listener); } - - @Override - public void addIssueListener(IssueListener listener) { - // do nothing - } - - @Override - public void removeIssueListener(IssueListener listener) { - // do nothing - } - - @Override - public void reportIssue(Issue issue) { - // do nothing - } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorSplitter.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorSplitter.java index e893e7a766..f62153d403 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorSplitter.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TaskMonitorSplitter.java @@ -19,7 +19,6 @@ import java.util.HashSet; import java.util.Set; import generic.concurrent.ConcurrentListenerSet; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; public class TaskMonitorSplitter { @@ -176,20 +175,5 @@ public class TaskMonitorSplitter { listener.cancelled(); } } - - @Override - public void addIssueListener(IssueListener listener) { - parent.addIssueListener(listener); - } - - @Override - public void removeIssueListener(IssueListener listener) { - parent.removeIssueListener(listener); - } - - @Override - public void reportIssue(Issue issue) { - parent.reportIssue(issue); - } } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java index e5cf10f7ed..d2598da787 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/TimeoutTaskMonitor.java @@ -20,7 +20,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import ghidra.generic.function.Callback; -import ghidra.util.Issue; import ghidra.util.SystemUtilities; import ghidra.util.exception.CancelledException; import ghidra.util.exception.TimeoutException; @@ -184,11 +183,6 @@ public class TimeoutTaskMonitor implements TaskMonitor { return delegate.getProgress(); } - @Override - public void reportIssue(Issue issue) { - delegate.reportIssue(issue); - } - private void timeout() { didTimeout.set(true); timeoutCallback.call(); @@ -225,14 +219,4 @@ public class TimeoutTaskMonitor implements TaskMonitor { public void clearCanceled() { delegate.clearCanceled(); } - - @Override - public void addIssueListener(IssueListener listener) { - delegate.addIssueListener(listener); - } - - @Override - public void removeIssueListener(IssueListener listener) { - delegate.removeIssueListener(listener); - } } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/WrappingTaskMonitor.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/WrappingTaskMonitor.java index 71e34965ce..6e5761d9d7 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/WrappingTaskMonitor.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/task/WrappingTaskMonitor.java @@ -15,7 +15,6 @@ */ package ghidra.util.task; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; /** @@ -91,11 +90,6 @@ public class WrappingTaskMonitor implements TaskMonitor { return delegate.getProgress(); } - @Override - public void reportIssue(Issue issue) { - delegate.reportIssue(issue); - } - @Override public void cancel() { delegate.cancel(); @@ -125,14 +119,4 @@ public class WrappingTaskMonitor implements TaskMonitor { public void clearCanceled() { delegate.clearCanceled(); } - - @Override - public void addIssueListener(IssueListener listener) { - delegate.addIssueListener(listener); - } - - @Override - public void removeIssueListener(IssueListener listener) { - delegate.removeIssueListener(listener); - } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LockingTaskMonitor.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LockingTaskMonitor.java index ef9e909b7a..9b2afafbbb 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LockingTaskMonitor.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/data/LockingTaskMonitor.java @@ -15,10 +15,7 @@ */ package ghidra.framework.data; -import ghidra.util.Issue; import ghidra.util.SystemUtilities; -import ghidra.util.datastruct.WeakDataStructureFactory; -import ghidra.util.datastruct.WeakSet; import ghidra.util.exception.CancelledException; import ghidra.util.task.*; @@ -35,7 +32,6 @@ class LockingTaskMonitor implements TaskMonitor { private boolean showProgressValue = true; private String msg; private MyTaskDialog taskDialog; - private WeakSet issueListeners; /** * Constructs a locking task handler for a locked dobj. The setCompleted() method must be @@ -263,28 +259,4 @@ class LockingTaskMonitor implements TaskMonitor { public void removeCancelledListener(CancelledListener listener) { throw new UnsupportedOperationException(); } - - @Override - public void addIssueListener(IssueListener listener) { - if (issueListeners == null) { - issueListeners = WeakDataStructureFactory.createCopyOnWriteWeakSet(); - } - } - - @Override - public void removeIssueListener(IssueListener listener) { - if (issueListeners != null) { - issueListeners.remove(listener); - } - - } - - @Override - public void reportIssue(Issue issue) { - if (issueListeners != null) { - for (IssueListener listener : issueListeners) { - listener.issueReported(issue); - } - } - } } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/task/GTaskMonitor.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/task/GTaskMonitor.java index 84888d3450..deb0fd449a 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/task/GTaskMonitor.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/task/GTaskMonitor.java @@ -16,9 +16,9 @@ package ghidra.framework.task; import ghidra.framework.task.gui.GProgressBar; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; -import ghidra.util.task.*; +import ghidra.util.task.CancelledListener; +import ghidra.util.task.TaskMonitor; /** * Implementation of a TaskMontor that can be "attached" to a GProgressBar. @@ -123,11 +123,6 @@ public class GTaskMonitor implements TaskMonitor, CancelledListener { return progress; } - @Override - public void reportIssue(Issue issue) { - // do nothing for now; - } - @Override public void cancel() { if (cancelEnabled) { @@ -145,16 +140,6 @@ public class GTaskMonitor implements TaskMonitor, CancelledListener { throw new UnsupportedOperationException(); } - @Override - public void addIssueListener(IssueListener listener) { - // not currently supported - } - - @Override - public void removeIssueListener(IssueListener listener) { - // not currently supported - } - @Override public void setCancelEnabled(boolean enable) { cancelEnabled = enable; diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java index 6a536dda2e..45e3f96f6f 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/database/ProgramDB.java @@ -281,9 +281,11 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM throws IOException, VersionException, LanguageNotFoundException, CancelledException { super(dbh, "Untitled", 500, 1000, consumer); + if (monitor == null) { - monitor = TaskMonitorAdapter.DUMMY_MONITOR; + monitor = TaskMonitorAdapter.DUMMY; } + boolean success = false; try { int id = startTransaction("create program"); @@ -294,6 +296,7 @@ public class ProgramDB extends DomainObjectAdapterDB implements Program, ChangeM VersionException dbVersionExc = initializeDatabase(openMode); VersionException languageVersionExc = null; + try { language = DefaultLanguageService.getLanguageService().getLanguage(languageID); languageVersionExc = checkLanguageVersion(openMode); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageProvider.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageProvider.java index 4ff69c49e2..a94c852ae7 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageProvider.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageProvider.java @@ -28,18 +28,19 @@ public interface LanguageProvider extends ExtensionPoint { /** * Returns the language with the given name or null if no language has that name - * @param name the name of the language to be retrieved - * @return the language with the given name + * + * @param languageId the name of the language to be retrieved + * @return the {@link Language} with the given name */ Language getLanguage(LanguageID languageId); - + /** * Returns a list of language descriptions provided by this provider */ LanguageDescription[] getLanguageDescriptions(); /** - * @return true if one of more laguages or language description failed to load + * @return true if one of more languages or language description failed to load * properly. */ boolean hadLoadFailure(); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageService.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageService.java index 1fde4fe325..3fce1b14e0 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageService.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/model/lang/LanguageService.java @@ -27,12 +27,13 @@ import java.util.List; public interface LanguageService { /** - * Returns the language with the given language ID. - * @param languageID the ID of language to retrieve. - * @throws LanguageNotFoundException if no language can be found for the given ID. + * Returns the language with the given language ID + * @param languageID the ID of language to retrieve + * @return the {@link Language} matching the given ID + * @throws LanguageNotFoundException if no language can be found for the given ID */ Language getLanguage(LanguageID languageID) throws LanguageNotFoundException; - + /** * Returns the default Language to use for the given processor; * @param processor the processor for which to get a language. @@ -59,13 +60,14 @@ public interface LanguageService { /** * Returns all known language descriptions which satisfy the criteria identify by the * non-null parameters. A null value implies a don't-care wildcard value. - * @param processor - * @param endianess - * @param size - * @param variant - * @return - * @deprecated + * @param processor the processor for which to get a language + * @param endianess big or little + * @param size processor address space size (in bits) + * @param variant the processor version (usually 'default') + * @return the language descriptions that fit the parameters + * @deprecated use {@link #getLanguageDescriptions(Processor)} instead */ + @Deprecated List getLanguageDescriptions(Processor processor, Endian endianess, Integer size, String variant); diff --git a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/DefaultLanguageService.java b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/DefaultLanguageService.java index 6213246a90..1ab12feea4 100644 --- a/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/DefaultLanguageService.java +++ b/Ghidra/Framework/SoftwareModeling/src/main/java/ghidra/program/util/DefaultLanguageService.java @@ -27,6 +27,8 @@ import generic.jar.ResourceFile; import ghidra.app.plugin.processors.sleigh.SleighLanguageProvider; import ghidra.program.model.lang.*; import ghidra.util.classfinder.ClassSearcher; +import ghidra.util.task.TaskMonitor; +import ghidra.util.task.TaskMonitorService; /** * Default Language service used gather up all the languages that were found @@ -43,6 +45,7 @@ public class DefaultLanguageService implements LanguageService, ChangeListener { /** * Returns the single instance of the DefaultLanguageService. + * @return the language service */ public static synchronized LanguageService getLanguageService() { if (languageService == null) { @@ -94,13 +97,23 @@ public class DefaultLanguageService implements LanguageService, ChangeListener { */ @Override public Language getLanguage(LanguageID languageID) throws LanguageNotFoundException { - LanguageInfo info = languageMap.get(languageID); - if (info == null) { - throw new LanguageNotFoundException(languageID); + TaskMonitor monitor = TaskMonitorService.getMonitor(); + monitor.setMessage("Retrieving language: " + languageID); + + try { + LanguageInfo info = languageMap.get(languageID); + + if (info == null) { + throw new LanguageNotFoundException(languageID); + } + + Language lang = info.getLanguage(); + return lang; + } + finally { + monitor.setMessage(""); } - - return info.getLanguage(); } /** @@ -161,8 +174,9 @@ public class DefaultLanguageService implements LanguageService, ChangeListener { private static boolean languageMatchesExternalProcessor(LanguageDescription description, String externalProcessorName, String externalTool) { boolean result = false; - if (externalProcessorName == null) + if (externalProcessorName == null) { result = true; + } else if (externalTool != null) { List extNames = description.getExternalNames(externalTool); if (extNames != null) { diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/StubTaskMonitor.java b/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/StubTaskMonitor.java index f295a8a5d2..5a8983b3d3 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/StubTaskMonitor.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/StubTaskMonitor.java @@ -15,7 +15,6 @@ */ package ghidra.util.task; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; class StubTaskMonitor implements TaskMonitor { @@ -83,12 +82,6 @@ class StubTaskMonitor implements TaskMonitor { return 0; } - @Override - public void reportIssue(Issue issue) { - // stub - - } - @Override public void cancel() { // stub @@ -107,18 +100,6 @@ class StubTaskMonitor implements TaskMonitor { } - @Override - public void addIssueListener(IssueListener listener) { - // stub - - } - - @Override - public void removeIssueListener(IssueListener listener) { - // stub - - } - @Override public void setCancelEnabled(boolean enable) { // stub diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitor.java b/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitor.java index ca992731f6..94484377e4 100644 --- a/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitor.java +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitor.java @@ -15,49 +15,93 @@ */ package ghidra.util.task; -import ghidra.util.Issue; import ghidra.util.exception.CancelledException; /** * TaskMonitor provides an interface by means of which a * potentially long running task can show its progress and also check if the user - * has cancelled the operation. Operations that support a task monitor should periodically + * has cancelled the operation. + *

+ * Operations that support a task monitor should periodically * check to see if the operation has been cancelled and abort. If possible, the * operation should also provide periodic progress information. If it can estimate a * percentage done, then it should use the setProgress(int) method, - * otherwise it - * should just call the setMessage(String) method. + * otherwise it should just call the setMessage(String) method. */ public interface TaskMonitor { public static final TaskMonitor DUMMY = new StubTaskMonitor(); - /** - * A value to indicate that this monitor has no progress value set. - */ + /** A value to indicate that this monitor has no progress value set */ public static final int NO_PROGRESS_VALUE = -1; /** - * Returns true if the user has cancelled the operation. + * Returns true if the user has cancelled the operation + * + * @return true if the user has cancelled the operation */ public boolean isCancelled(); /** - * True (the default) signals to paint the progress information inside of the progress bar. + * Returns true if the monitor has been initialized + * + * @return true if the monitor has been initialized + */ + public default boolean isInitialized() { + return false; + } + + /** + * Sets the initialization state of the monitor + * + * @param init true for initialized, false otherwise + */ + public default void setInitialized(boolean init) { + // do nothing - this is defaulted for backward compatibility so current + // task monitor implementations do not have to change + } + + /** + * Restores the monitor to an uninitialized state. This will result in the primary + * monitor being returned from the {@link TaskMonitorService} on the next + * invocation. + */ + public default void reset() { + synchronized (this) { + setMessage(""); + setProgress(0); + setMaximum(0); + setInitialized(false); + clearCanceled(); + } + } + + /** + * True (the default) signals to paint the progress information inside of the progress bar * * @param showProgressValue true to paint the progress value; false to not */ public void setShowProgressValue(boolean showProgressValue); /** - * Sets a message giving additional information about the current - * progress. - * @param message more information + * Sets the message displayed on the task monitor + * + * @param message the message to display */ public void setMessage(String message); + + /** + * Returns a version of this monitor that cannot have its progress state changed. This is + * meant for sub-tasks that should not be allowed to hijack task progress. + * + * @return null + */ + public default TaskMonitor getSecondaryMonitor() { + return null; + } /** - * Sets the current progress value. + * Sets the current progress value * @param value progress value */ public void setProgress(long value); @@ -71,7 +115,7 @@ public interface TaskMonitor { public void initialize(long max); /** - * Set the progress maximum value. + * Set the progress maximum value *

* Note: setting this value will reset the progress to be the max if the progress is currently * greater than the new new max value. @@ -80,88 +124,70 @@ public interface TaskMonitor { public void setMaximum(long max); /** - * Returns the current maximum value for progress. - * @return + * Returns the current maximum value for progress + * @return the maximum progress value */ public long getMaximum(); /** - * An indeterminate task monitor may choose to show an animation instead of updating progress. + * An indeterminate task monitor may choose to show an animation instead of updating progress + * @param indeterminate true if indeterminate */ public void setIndeterminate(boolean indeterminate); /** - * Check to see if this monitor has been canceled. - * @throws CancelledException if monitor has been cancelled. + * Check to see if this monitor has been canceled + * @throws CancelledException if monitor has been cancelled */ public void checkCanceled() throws CancelledException; /** - * A convenience method to increment the current progress by the given value. - * @param incrementAmount The amount by which to increment the progress. + * A convenience method to increment the current progress by the given value + * @param incrementAmount The amount by which to increment the progress */ public void incrementProgress(long incrementAmount); /** * Returns the current progress value or {@link #NO_PROGRESS_VALUE} if there is no value - * set. + * set * @return the current progress value or {@link #NO_PROGRESS_VALUE} if there is no value - * set. + * set */ public long getProgress(); /** - * Notify that an issue occurred while processing. - * @param issue the issue that was encountered - */ - public void reportIssue(Issue issue); - - /** - * Cancel the task. + * Cancel the task */ public void cancel(); /** - * Add cancelled listener. - * @param listener + * Add cancelled listener + * @param listener the cancel listener */ public void addCancelledListener(CancelledListener listener); /** - * Remove cancelled listener. - * @param listener + * Remove cancelled listener + * @param listener the cancel listener */ public void removeCancelledListener(CancelledListener listener); /** - * Set the enablement of the Cancel button. + * Set the enablement of the Cancel button * @param enable true means to enable the cancel button */ public void setCancelEnabled(boolean enable); /** * Returns true if cancel ability is enabled + * @return true if cancel ability is enabled */ public boolean isCancelEnabled(); /** - * Clear the cancellation so that this TaskMonitor may be reused. + * Clear the cancellation so that this TaskMonitor may be reused * */ public void clearCanceled(); - /** - * Add an issue listener to this monitor. - * - * @param listener the listener - */ - public void addIssueListener(IssueListener listener); - - /** - * Removes an issue listener to this monitor. - * - * @param listener the listener - */ - public void removeIssueListener(IssueListener listener); - } diff --git a/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitorService.java b/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitorService.java new file mode 100644 index 0000000000..efdb06ea84 --- /dev/null +++ b/Ghidra/Framework/Utility/src/main/java/ghidra/util/task/TaskMonitorService.java @@ -0,0 +1,154 @@ +/* ### + * 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 ghidra.util.task; + +import java.util.concurrent.atomic.AtomicInteger; + +import javax.swing.SwingUtilities; + +/** + * Provides access to the {@link TaskMonitor} instance for the current thread. The first + * time a monitor is requested via {@link #getMonitor()}, a "primary" monitor (one + * that allows updating of task progress and status messages) is returned; all + * subsequent requests will return a "secondary" monitor, which only allows + * status message updates. This is to keep the progress bar from being updated + * simultaneously by multiple parties. + *

+ * Note: {@link TaskMonitor monitor} instances are registered with this service via the + * {@link #register(TaskMonitor) setMonitor} call, and will be available to that thread until + * the {@link #remove(int) remove} method is called. + *

+ * Note: Because monitor instances are managed by a {@link ThreadLocal} object, they will be + * cleaned up automatically by the GC when the thread is terminated. + */ +public class TaskMonitorService { + + /** + * The {@link TaskMonitor} instance. ThreadLocal ensures that each thread has access + * to its own monitor. + */ + private static ThreadLocal localMonitor = new ThreadLocal() { + + /** + * Force the initial value to be null so users will have to call + * {@link TaskMonitorService#register(TaskMonitor) register} to assign one + * + * @return null + */ + @Override + protected TaskMonitor initialValue() { + return null; + } + }; + + /** + * Unique id for each thread monitor that is assigned when a monitor is + * {@link #register(TaskMonitor) registered}. This is to ensure that only clients who have + * a valid id can remove a monitor. + */ + private static ThreadLocal localMonitorId = new ThreadLocal() { + + @Override + protected Integer initialValue() { + return nextId.getAndIncrement(); + } + }; + + /** + * Contains the next unique id for the monitor; this is updated each time a new monitor + * is registered with the service. + */ + private static final AtomicInteger nextId = new AtomicInteger(0); + + /** + * Returns the task monitor for the current thread. If one has not yet been registered, + * a {@link StubTaskMonitor stub monitor} is returned. + * + * @return the task monitor + */ + public synchronized static TaskMonitor getMonitor() { + + if (localMonitor.get() == null) { + + // If no monitor is available, just return a stub. The alternative is to throw an + // exception but this isn't considered an error condition in all cases. + localMonitor.set(new StubTaskMonitor()); + } + + // If the monitor has already been initialized, return the secondary monitor to prevent + // the caller from hijacking the progress bar + if (localMonitor.get().isInitialized()) { + return localMonitor.get().getSecondaryMonitor(); + } + + // This ensures that the next time this method is called, the service + // will return the secondary monitor + localMonitor.get().setInitialized(true); + + return localMonitor.get(); + } + + /** + * Sets the given monitor for this thread + * + * @param monitor the task monitor to register + * @return the unique id for the monitor + */ + public static int register(TaskMonitor monitor) { + + // Don't allow callers to register a monitor if on the swing thread + if (SwingUtilities.isEventDispatchThread()) { + throw new IllegalArgumentException("Attempting to set a monitor in the Swing thread!"); + } + + // Don't allow users to register a monitor if there is already one registered for this + // thread + if (localMonitor.get() != null) { + throw new IllegalArgumentException("Task monitor already assigned to this thread"); + } + + localMonitor.set(monitor); + + return localMonitorId.get(); + + } + + /** + * Removes the monitor from the thread local object. To protect against clients cavalierly + * removing monitors, a valid monitor id must be provided; this is generated at the time + * of monitor {@link #register(TaskMonitor) registration}. + *

+ * Note: This should generally not need to be called as the GC will clean up thread local + * objects when the associated thread is finished. + * + * @param monitorId the unique ID for the monitor to be removed + */ + public static void remove(int monitorId) { + + if (monitorId != localMonitorId.get()) { + throw new IllegalArgumentException("Invalid monitor id for this thread: " + monitorId); + } + + localMonitor.remove(); + } + + /** + * Hide the constructor - this should not be instantiated + */ + private TaskMonitorService() { + // nothing to do + } +}