--- old/application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/messages/internal/Messages.java 2019-04-05 13:01:44.978833678 -0400 +++ new/application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/messages/internal/Messages.java 2019-04-05 13:01:44.911832968 -0400 @@ -489,6 +489,8 @@ public static String TABLECOMPONENT_HISTOGRAM_SELECTION; public static String TABLECOMPONENT_NONE; public static String ThreadDumpsPage_PAGE_NAME; + public static String ThreadsPage_HIDE_THREAD_ACTION; + public static String ThreadsPage_RESET_CHART_TO_SELECTION_ACTION; public static String ThreadsPage_EDIT_LANES; public static String ThreadsPage_LANE_TOOLTIP_TITLE; public static String ThreadsPage_NAME; --- old/application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/pages/ThreadsPage.java 2019-04-05 13:01:45.240836456 -0400 +++ new/application/org.openjdk.jmc.flightrecorder.ui/src/main/java/org/openjdk/jmc/flightrecorder/ui/pages/ThreadsPage.java 2019-04-05 13:01:45.173835745 -0400 @@ -76,7 +76,9 @@ import org.openjdk.jmc.flightrecorder.ui.common.ItemRow; import org.openjdk.jmc.flightrecorder.ui.common.ThreadGraphLanes; import org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages; +import org.openjdk.jmc.ui.UIPlugin; import org.openjdk.jmc.ui.charts.IXDataRenderer; +import org.openjdk.jmc.ui.charts.QuantitySpanRenderer; import org.openjdk.jmc.ui.charts.RendererToolkit; import org.openjdk.jmc.ui.column.ColumnManager.SelectionState; import org.openjdk.jmc.ui.column.TableSettings; @@ -152,15 +154,23 @@ private class ThreadsPageUi extends ChartAndTableUI { private static final String THREADS_TABLE_FILTER = "threadsTableFilter"; //$NON-NLS-1$ + private static final String HIDE_THREAD = "hideThread"; //$NON-NLS-1$ + private static final String RESET_CHART = "resetChart"; //$NON-NLS-1$ + private IAction hideThreadAction; + private IAction resetChartAction; private ThreadGraphLanes lanes; private MCContextMenuManager mm; + private List threadRows; + private Boolean reloadThreads; + private Boolean isChartModified; + private Boolean isChartMenuActionsInit; ThreadsPageUi(Composite parent, FormToolkit toolkit, IPageContainer editor, IState state) { super(pageFilter, getDataSource(), parent, toolkit, editor, state, getName(), pageFilter, getIcon(), flavorSelectorState); mm = (MCContextMenuManager) chartCanvas.getContextMenu(); sash.setOrientation(SWT.HORIZONTAL); - mm.add(new Separator()); + addActionsToContextMenu(mm); // FIXME: The lanes field is initialized by initializeChartConfiguration which is called by the super constructor. This is too indirect for SpotBugs to resolve and should be simplified. lanes.updateContextMenu(mm, false); @@ -176,6 +186,86 @@ onFilterChange(tableFilter); } + /** + * Hides a thread from the chart and rebuilds the chart + */ + private void hideThread(String threadName) { + if (this.threadRows != null && this.threadRows.size() > 0) { + int index = indexOfThreadName(threadName); + if (index != -1) { + this.threadRows.remove(index); + this.reloadThreads = false; + buildChart(); + if (!this.isChartModified) { + this.isChartModified = true; + setResetChartActionEnablement(true); + } + } + if (this.threadRows.size() == 0) { + setHideThreadActionEnablement(false); + } + } + } + + /** + * Locates the index of the target Thread (by name) in the current selection list + * + * @param name + * the name of the Thread of interest + * @return the index of the Thread in the current selection, or -1 if not found + */ + private int indexOfThreadName(String name) { + for (int i = 0; i < this.threadRows.size() && name != null; i++) { + if (this.threadRows.get(i) instanceof QuantitySpanRenderer) { + if (name.equals(((QuantitySpanRenderer) this.threadRows.get(i)).getName())) { + return i; + } + } + } + return -1; + } + + /** + * Adds the hide thread and reset chart actions to the context menu + */ + private void addActionsToContextMenu(MCContextMenuManager mm) { + mm.add(new Separator()); + + IAction hideThreadAction = ActionToolkit.action(() -> this.hideThread(chartCanvas.getActiveScopeName()), + Messages.ThreadsPage_HIDE_THREAD_ACTION, + UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_DELETE)); + hideThreadAction.setId(HIDE_THREAD); + this.hideThreadAction = hideThreadAction; + mm.add(hideThreadAction); + + IAction resetChartAction = ActionToolkit.action(() -> this.resetChartToSelection(), + Messages.ThreadsPage_RESET_CHART_TO_SELECTION_ACTION, + UIPlugin.getDefault().getMCImageDescriptor(UIPlugin.ICON_REFRESH)); + resetChartAction.setId(RESET_CHART); + resetChartAction.setEnabled(this.isChartModified); + this.resetChartAction = resetChartAction; + mm.add(resetChartAction); + + this.isChartMenuActionsInit = true; + } + + /** + * Redraws the chart, and disables the reset chart menu action + */ + private void resetChartToSelection() { + buildChart(); + this.isChartModified = false; + setResetChartActionEnablement(false); + setHideThreadActionEnablement(true); + } + + private void setHideThreadActionEnablement(Boolean enabled) { + this.hideThreadAction.setEnabled(enabled); + } + private void setResetChartActionEnablement(Boolean enabled) { + this.resetChartAction.setEnabled(enabled); + } + @Override protected ItemHistogram buildHistogram(Composite parent, IState state) { ItemHistogram build = HISTOGRAM.buildWithoutBorder(parent, JfrAttributes.EVENT_THREAD, @@ -198,15 +288,25 @@ } boolean useDefaultSelection = rows.size() > 1; if (lanes.getLaneDefinitions().stream().anyMatch(a -> a.isEnabled()) && selection.getRowCount() > 0) { - List threadRows = selection - .getSelectedRows((object, items) -> lanes.buildThreadRenderer(object, items)) - .collect(Collectors.toList()); + if (this.reloadThreads) { + this.threadRows = selection + .getSelectedRows((object, items) -> lanes.buildThreadRenderer(object, items)) + .collect(Collectors.toList()); + this.isChartModified = false; + if (this.isChartMenuActionsInit) { + setResetChartActionEnablement(false); + setHideThreadActionEnablement(true); + } + } else { + this.reloadThreads = true; + } + double threadsWeight = Math.sqrt(threadRows.size()) * 0.15; double otherRowWeight = Math.max(threadsWeight * 0.1, (1 - threadsWeight) / rows.size()); List weights = Stream .concat(Stream.generate(() -> otherRowWeight).limit(rows.size()), Stream.of(threadsWeight)) .collect(Collectors.toList()); - rows.add(RendererToolkit.uniformRows(threadRows)); + rows.add(RendererToolkit.uniformRows(this.threadRows)); useDefaultSelection = true; rows = Arrays.asList(RendererToolkit.weightedRows(rows, weights)); } @@ -236,6 +336,9 @@ @Override protected List initializeChartConfiguration(IState state) { + this.reloadThreads = true; + this.isChartModified = false; + this.isChartMenuActionsInit = false; lanes = new ThreadGraphLanes(() -> getDataSource(), () -> buildChart()); return lanes.initializeChartConfiguration(Stream.of(state.getChildren(THREAD_LANE))); } --- old/application/org.openjdk.jmc.flightrecorder.ui/src/main/resources/org/openjdk/jmc/flightrecorder/ui/messages/internal/messages.properties 2019-04-05 13:01:45.501839222 -0400 +++ new/application/org.openjdk.jmc.flightrecorder.ui/src/main/resources/org/openjdk/jmc/flightrecorder/ui/messages/internal/messages.properties 2019-04-05 13:01:45.436838533 -0400 @@ -475,7 +475,8 @@ TABLECOMPONENT_NONE=[None] ThreadDumpsPage_PAGE_NAME=Thread Dumps - +ThreadsPage_HIDE_THREAD_ACTION=Hide Thread From The Chart +ThreadsPage_RESET_CHART_TO_SELECTION_ACTION=Reset The Chart To Current Selection ThreadsPage_EDIT_LANES=Edit Thread Lanes # {0} is the thread name, {1} is the lane name ThreadsPage_LANE_TOOLTIP_TITLE={0} / {1} Lane --- old/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/IChartInfoVisitor.java 2019-04-05 13:01:45.770842073 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/IChartInfoVisitor.java 2019-04-05 13:01:45.702841353 -0400 @@ -147,6 +147,8 @@ Object getPayload(); String getDescription(); + + String getName(); } public interface ITick { --- old/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/Messages.java 2019-04-05 13:01:46.023844755 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/Messages.java 2019-04-05 13:01:45.958844066 -0400 @@ -41,6 +41,7 @@ private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); public static final String RendererToolkit_TOO_MUCH_CONTENT = "RendererToolkit_TOO_MUCH_CONTENT"; //$NON-NLS-1$ + public static final String RendererToolkit_NO_CONTENT = "RendererToolkit_NO_CONTENT"; //$NON-NLS-1$ private Messages() { } --- old/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/QuantitySpanRenderer.java 2019-04-05 13:01:46.273847405 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/QuantitySpanRenderer.java 2019-04-05 13:01:46.207846706 -0400 @@ -75,6 +75,10 @@ return Math.min(5, (height + 10) / 20); } + public String getName() { + return text; + } + @Override public IRenderedRow render(Graphics2D context, SubdividedQuantityRange xRange, int height) { int margin = calcMargin(height); @@ -101,6 +105,7 @@ private final Paint paint; private final int margin; private String description; + private String text; public QuantitySpanRendering(int margin, XYQuantities points, IRenderedRow content, Paint paint, String text, String description) { @@ -111,12 +116,14 @@ this.content = content; this.paint = paint; this.description = description; + this.text = text; } @Override public void infoAt(IChartInfoVisitor visitor, int x, int y, Point offset) { offset = new Point(offset.x, offset.y + margin); content.infoAt(visitor, x, y, offset); + visitor.enterScope(text, true); // FIXME: Only output this if near the boundaries? At least handle infinite lengths. if (points != null) { @@ -174,6 +181,11 @@ } @Override + public String getName() { + return text; + } + + @Override public String getDescription() { return description; } --- old/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/RendererToolkit.java 2019-04-05 13:01:46.523850055 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/RendererToolkit.java 2019-04-05 13:01:46.458849366 -0400 @@ -102,9 +102,11 @@ private static class CompositeRenderer implements IXDataRenderer { - private static final Color TOO_MUCH_CONTENT_BG = new Color(240, 240, 240, 190); + private static final Color MISMATCH_CONTENT_BG = new Color(240, 240, 240, 190); private static final String TOO_MUCH_CONTENT_MSG = Messages .getString(Messages.RendererToolkit_TOO_MUCH_CONTENT); + private static final String NO_CONTENT_MSG = Messages + .getString(Messages.RendererToolkit_NO_CONTENT); private final List children; private final List weights; private final String text; @@ -143,18 +145,19 @@ } context.setTransform(oldTransform); if (result.size() != children.size()) { + String displayMessage = result.size() == 0 ? NO_CONTENT_MSG : TOO_MUCH_CONTENT_MSG; result = Collections.emptyList(); - context.setPaint(TOO_MUCH_CONTENT_BG); + context.setPaint(MISMATCH_CONTENT_BG); context.fillRect(0, 0, xRange.getPixelExtent(), height); // FIXME: Draw something nice. Font orgFont = context.getFont(); Font italicFont = orgFont.deriveFont(Font.ITALIC); FontMetrics fm = context.getFontMetrics(italicFont); - int msgWidth = fm.stringWidth(TOO_MUCH_CONTENT_MSG); + int msgWidth = fm.stringWidth(displayMessage); if (height > fm.getHeight() && xRange.getPixelExtent() > msgWidth) { context.setFont(italicFont); context.setPaint(Color.BLACK); - context.drawString(TOO_MUCH_CONTENT_MSG, (xRange.getPixelExtent() - msgWidth) / 2, + context.drawString(displayMessage, (xRange.getPixelExtent() - msgWidth) / 2, (height - fm.getHeight()) / 2 + fm.getAscent()); context.setFont(orgFont); } --- old/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/SpanRenderer.java 2019-04-05 13:01:46.773852705 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/charts/SpanRenderer.java 2019-04-05 13:01:46.709852027 -0400 @@ -148,6 +148,11 @@ return description; } + @Override + public String getName() { // In this case, the description is the name + return description; + } + } } --- old/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/misc/ChartCanvas.java 2019-04-05 13:01:47.032855450 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/java/org/openjdk/jmc/ui/misc/ChartCanvas.java 2019-04-05 13:01:46.967854761 -0400 @@ -77,6 +77,7 @@ private int lastMouseX = -1; private int lastMouseY = -1; private List highlightRects; + private String activeScopeName; private class Selector extends MouseAdapter implements MouseMoveListener, MouseTrackListener { @@ -152,6 +153,7 @@ @Override public void mouseExit(MouseEvent e) { + resetActiveScopeName(); clearHighlightRects(); } @@ -391,6 +393,18 @@ return (int) Math.round(x / xScale); } + public String getActiveScopeName() { + return this.activeScopeName; + } + + public void resetActiveScopeName() { + this.activeScopeName = null; + } + + public void setActiveScopeName(String name) { + this.activeScopeName = name; + } + private void updateHighlightRects() { List newRects = new ArrayList<>(); infoAt(new IChartInfoVisitor.Adapter() { @@ -420,6 +434,12 @@ public void visit(ILane lane) { // FIXME: Do we want this highlighted? } + + @Override + public boolean enterScope(String context, boolean fullyShown) { + setActiveScopeName(context); + return false; + } }, lastMouseX, lastMouseY); // Attempt to reduce flicker by avoiding unnecessary updates. if (!newRects.equals(highlightRects)) { --- old/application/org.openjdk.jmc.ui/src/main/resources/org/openjdk/jmc/ui/charts/messages.properties 2019-04-05 13:01:47.286858143 -0400 +++ new/application/org.openjdk.jmc.ui/src/main/resources/org/openjdk/jmc/ui/charts/messages.properties 2019-04-05 13:01:47.222857464 -0400 @@ -31,3 +31,4 @@ # WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # RendererToolkit_TOO_MUCH_CONTENT=Too much content... +RendererToolkit_NO_CONTENT=There is no content to display... --- old/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/base/wrappers/MCJemmyBase.java 2019-04-05 13:01:47.536860792 -0400 +++ new/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/base/wrappers/MCJemmyBase.java 2019-04-05 13:01:47.471860104 -0400 @@ -51,9 +51,12 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Widget; +import org.jemmy.Point; import org.jemmy.action.AbstractExecutor; import org.jemmy.control.Wrap; import org.jemmy.env.Environment; @@ -71,6 +74,7 @@ import org.jemmy.interfaces.Keyboard.KeyboardButton; import org.jemmy.interfaces.Keyboard.KeyboardButtons; import org.jemmy.interfaces.Keyboard.KeyboardModifiers; +import org.jemmy.interfaces.Mouse.MouseButtons; import org.jemmy.interfaces.Parent; import org.jemmy.lookup.AbstractLookup; import org.jemmy.lookup.Lookup; @@ -530,6 +534,45 @@ } /** + * Opens the context menu by right-clicking at a provided point + * + * @param p + * the point at which to right-click and open the context menu + */ + private void openContextMenuAtPoint(Point p) { + Display.getDefault().syncExec(() -> { + control.mouse().click(1, p, MouseButtons.BUTTON3); + }); + } + + /** + * Checks the isEnabled value for a menu item + * + * @param p + * the point on the screen at which to open the context menu + * @param menuItemText + * the menu item of interest + * @return the isEnabled value for the menu item of interest + */ + public boolean isContextMenuItemEnabled(Point p, String menuItemText) { + openContextMenuAtPoint(p); + Fetcher fetcher = new Fetcher() { + @Override + public void run() { + Menu menu = control.getControl().getMenu(); + for (MenuItem item : menu.getItems()) { + if(menuItemText.equals(item.getText())) { + setOutput(item.isEnabled()); + break; + } + } + } + }; + Display.getDefault().syncExec(fetcher); + return (fetcher.getOutput() == null) ? false : fetcher.getOutput(); + } + + /** * Convenience method to find out if a control is visible * * @param controlWrap --- old/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/wrappers/JfrUi.java 2019-04-05 13:01:47.805863644 -0400 +++ new/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/wrappers/JfrUi.java 2019-04-05 13:01:47.738862934 -0400 @@ -56,6 +56,7 @@ */ public static enum Tabs { JAVA_APPLICATION, + THREADS, MEMORY, LOCK_INSTANCES, FILE_IO, @@ -98,6 +99,9 @@ case ALLOCATIONS: tabText = new String[] {"JVM Internals", "TLAB Allocations"}; break; + case THREADS: + tabText = new String[] {"Java Application", "Threads"}; + break; case MEMORY: tabText = new String[] {"Java Application", "Memory"}; break; --- old/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/wrappers/MCTable.java 2019-04-05 13:01:48.059866336 -0400 +++ new/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/wrappers/MCTable.java 2019-04-05 13:01:47.993865637 -0400 @@ -49,6 +49,7 @@ import org.jemmy.input.StringPopupOwner; import org.jemmy.input.StringPopupSelectableOwner; import org.jemmy.interfaces.Keyboard.KeyboardButtons; +import org.jemmy.interfaces.Keyboard.KeyboardModifiers; import org.jemmy.interfaces.Parent; import org.jemmy.interfaces.Selectable; import org.jemmy.lookup.Lookup; @@ -731,6 +732,22 @@ } /** + * Selects the table row at a specified "start" index, and uses SHIFT+DOWN to + * add to the selection up until a specified "end" index + * + * @param from + * the row index to start from + * @param to + * the row index to stop selecting + */ + public void selectItems(int start, int end) { + select(start); + for (int i = 1; i < end; i++) { + getShell().keyboard().pushKey(KeyboardButtons.DOWN, new KeyboardModifiers[] {KeyboardModifiers.SHIFT_DOWN_MASK}); + } + } + + /** * Context clicks the currently selected table item and chooses the supplied option * * @param desiredState --- /dev/null 2019-04-04 10:31:10.058999813 -0400 +++ new/application/uitests/org.openjdk.jmc.flightrecorder.uitest/src/test/java/org/openjdk/jmc/flightrecorder/uitest/JfrThreadsPageTest.java 2019-04-05 13:01:48.260868467 -0400 @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, Red Hat Inc. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at http://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.flightrecorder.uitest; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.openjdk.jmc.test.jemmy.MCJemmyTestBase; +import org.openjdk.jmc.test.jemmy.MCUITestRule; +import org.openjdk.jmc.test.jemmy.misc.wrappers.MCChartCanvas; +import org.openjdk.jmc.test.jemmy.misc.wrappers.JfrNavigator; +import org.openjdk.jmc.test.jemmy.misc.wrappers.JfrUi; +import org.openjdk.jmc.test.jemmy.misc.wrappers.MCMenu; +import org.openjdk.jmc.test.jemmy.misc.wrappers.MCTable; + +public class JfrThreadsPageTest extends MCJemmyTestBase { + + private static final String PLAIN_JFR = "plain_recording.jfr"; + private static final String TABLE_COLUMN_HEADER = "Thread"; + private static final String HIDE_THREAD = org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages.ThreadsPage_HIDE_THREAD_ACTION; + private static final String RESET_CHART = org.openjdk.jmc.flightrecorder.ui.messages.internal.Messages.ThreadsPage_RESET_CHART_TO_SELECTION_ACTION; + + private static MCChartCanvas chartCanvas; + private static MCTable threadsTable; + + @Rule + public MCUITestRule testRule = new MCUITestRule(verboseRuleOutput) { + @Override + public void before() { + JfrUi.openJfr(materialize("jfr", PLAIN_JFR, JfrThreadsPageTest.class)); + JfrNavigator.selectTab(JfrUi.Tabs.THREADS); + threadsTable = MCTable.getByColumnHeader(TABLE_COLUMN_HEADER); + chartCanvas = MCChartCanvas.getChartCanvas(); + } + + @Override + public void after() { + MCMenu.closeActiveEditor(); + } + }; + + @Test + public void testMenuItemEnablement() { + final int numThreads = threadsTable.getItemCount(); + Assert.assertTrue(numThreads > 0); + + Assert.assertFalse(chartCanvas.isContextMenuItemEnabled(RESET_CHART)); + Assert.assertTrue(chartCanvas.isContextMenuItemEnabled(HIDE_THREAD)); + + chartCanvas.clickContextMenuItem(HIDE_THREAD); + + Assert.assertTrue(chartCanvas.isContextMenuItemEnabled(RESET_CHART)); + Assert.assertTrue(chartCanvas.isContextMenuItemEnabled(HIDE_THREAD)); + + chartCanvas.clickContextMenuItem(RESET_CHART); + + Assert.assertFalse(chartCanvas.isContextMenuItemEnabled(RESET_CHART)); + Assert.assertTrue(chartCanvas.isContextMenuItemEnabled(HIDE_THREAD)); + } + + @Test + public void testHideAllThreads() { + final int numSelection = 7; + final int numThreads = threadsTable.getItemCount(); + Assert.assertTrue(numThreads > 0 && numThreads >= numSelection); + + // Select a limited number of threads in the chart using the table + threadsTable.selectItems(0, numSelection - 1); + + // Hide all the threads from the chart + for (int i = 0; i < numSelection; i++) { + chartCanvas.clickContextMenuItem(HIDE_THREAD); + } + + // Once all threads are hidden from the chart, the hide thread menu item will be disabled + Assert.assertFalse(chartCanvas.isContextMenuItemEnabled(HIDE_THREAD)); + Assert.assertTrue(chartCanvas.isContextMenuItemEnabled(RESET_CHART)); + + chartCanvas.clickContextMenuItem(RESET_CHART); + + // Verify the menu item isEnabled values are back to their default values + Assert.assertTrue(chartCanvas.isContextMenuItemEnabled(HIDE_THREAD)); + Assert.assertFalse(chartCanvas.isContextMenuItemEnabled(RESET_CHART)); + } +} \ No newline at end of file --- /dev/null 2019-04-04 10:31:10.058999813 -0400 +++ new/application/uitests/org.openjdk.jmc.test.jemmy/src/test/java/org/openjdk/jmc/test/jemmy/misc/wrappers/MCChartCanvas.java 2019-04-05 13:01:48.488870883 -0400 @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, Red Hat Inc. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at http://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.test.jemmy.misc.wrappers; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.openjdk.jmc.test.jemmy.misc.base.wrappers.MCJemmyBase; +import org.openjdk.jmc.test.jemmy.misc.fetchers.Fetcher; +import org.openjdk.jmc.ui.misc.ChartCanvas; +import org.jemmy.Point; +import org.jemmy.control.Wrap; +import org.jemmy.input.StringPopupOwner; +import org.jemmy.interfaces.Parent; +import org.jemmy.resources.StringComparePolicy; + +/** + * The Jemmy wrapper for the Mission Control Chart Canvas. + */ +public class MCChartCanvas extends MCJemmyBase { + + private MCChartCanvas(Wrap ChartCanvasWrap) { + this.control = ChartCanvasWrap; + } + + /** + * Returns all visible {@link MCChartCanvas} objects underneath the supplied shell + * + * @param shell + * the shell from where to start the search for the ChartCanvas object + * @return a {@link List} of {@link MCChartCanvas} objects + */ + @SuppressWarnings("unchecked") + public static List getAll(Wrap shell) { + List> list = getVisible(shell.as(Parent.class, ChartCanvas.class).lookup(ChartCanvas.class)); + List canvases = new ArrayList<>(); + for (int i = 0; i < list.size(); i++) { + canvases.add(new MCChartCanvas(list.get(i))); + } + return canvases; + } + + /** + * Returns the first visible {@link MCChartCanvas} object underneath the supplied shell + * + * @param shell + * the shell from where to start the search for the ChartCanvas object + * @return a {@link MCChartCanvas} object + */ + public static MCChartCanvas getFirst(Wrap shell) { + return getAll(shell).get(0); + } + + /** + * Returns the first visible {@link MCChartCanvas} object underneath the Mission Control main shell + * + * @return a {@link MCChartCanvas} object + */ + public static MCChartCanvas getChartCanvas() { + return getFirst(getShell()); + } + + /** + * Clicks a specific menu item in the context menu + * @param menuItemText + * the menu item of interest + */ + @SuppressWarnings("unchecked") + public void clickContextMenuItem(String menuItemText) { + StringPopupOwner contextMenu = control.as(StringPopupOwner.class); + contextMenu.setPolicy(StringComparePolicy.SUBSTRING); + contextMenu.push(getRelativeClickPoint(), new String[]{menuItemText}); + } + + /** + * Checks the isEnabled value for a menu item in the context menu + * + * @param menuItemText + * the menu item of interest + * @return the isEnabled value for the menu item of interest + */ + public boolean isContextMenuItemEnabled(String menuItemText) { + return this.isContextMenuItemEnabled(getRelativeClickPoint(), menuItemText); + } + + /** + * Calculates the click point of the Chart Canvas + * + * @return the Point of the Chart Canvas + */ + private Point getRelativeClickPoint() { + Fetcher fetcher = new Fetcher() { + @Override + public void run() { + setOutput(new Point(control.getScreenBounds().x, control.getScreenBounds().y)); + } + }; + Display.getDefault().syncExec(fetcher); + return fetcher.getOutput(); + } +}