You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by vl...@apache.org on 2019/10/05 10:33:04 UTC

[jmeter] branch master updated: Facelift ThreadGroup UI, improve alignment of Name+Commments fields for all components

This is an automated email from the ASF dual-hosted git repository.

vladimirsitnikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git


The following commit(s) were added to refs/heads/master by this push:
     new b0448c8  Facelift ThreadGroup UI, improve alignment of Name+Commments fields for all components
b0448c8 is described below

commit b0448c8cb3b70e7c3c58f84af926d0071a091f78
Author: Vladimir Sitnikov <si...@gmail.com>
AuthorDate: Fri Oct 4 10:59:37 2019 +0300

    Facelift ThreadGroup UI, improve alignment of Name+Commments fields for all components
---
 checksum.xml                                       |   1 +
 gradle.properties                                  |   2 +
 src/bom/build.gradle.kts                           |   3 +
 .../visualizers/RespTimeGraphVisualizer.java       |   2 +-
 .../jmeter/visualizers/StatGraphVisualizer.java    |   2 +-
 src/core/build.gradle.kts                          |   2 +
 .../jmeter/control/gui/LoopControlPanel.java       |  22 +++-
 .../jmeter/gui/AbstractJMeterGuiComponent.java     |  86 +++++++++-----
 .../java/org/apache/jmeter/gui/CommentPanel.java   |   6 +
 .../main/java/org/apache/jmeter/gui/NamePanel.java |   8 ++
 .../jmeter/threads/gui/AbstractThreadGroupGui.java |   4 +-
 .../apache/jmeter/threads/gui/ThreadGroupGui.java  | 131 ++++++---------------
 .../java/org/apache/jmeter/util/JMeterUtils.java   |  15 +++
 .../apache/jmeter/resources/messages.properties    |   6 +-
 src/licenses/build.gradle.kts                      |   8 ++
 src/licenses/licenses/miglayout/LICENSE            |  27 +++++
 xdocs/images/screenshots/setup_thread_group.png    | Bin 21075 -> 29834 bytes
 xdocs/images/screenshots/teardown_thread_group.png | Bin 21499 -> 110310 bytes
 .../screenshots/thread_group_distributed.png       | Bin 29000 -> 39480 bytes
 xdocs/images/screenshots/threadgroup.png           | Bin 22022 -> 31339 bytes
 xdocs/usermanual/component_reference.xml           |  10 +-
 21 files changed, 198 insertions(+), 137 deletions(-)

diff --git a/checksum.xml b/checksum.xml
index 65ca9a2..671127f 100644
--- a/checksum.xml
+++ b/checksum.xml
@@ -27,6 +27,7 @@
     <trusted-key id='e57428da9e879e7d' group='com.helger' />
     <trusted-key id='3684155e9365c30e' group='com.jayway.jsonpath' />
     <trusted-key id='a50569c7ca7fa1f0' group='com.jcraft' />
+    <trusted-key id='db45d3a62a183ce2' group='com.miglayout' />
     <trusted-key id='aa49c633b4734832' group='com.pinterest' />
     <trusted-key id='aa49c633b4734832' group='com.pinterest.ktlint' />
     <trusted-key id='1063fe98bcecb758' group='com.puppycrawl.tools' />
diff --git a/gradle.properties b/gradle.properties
index 2107116..20681f7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -32,6 +32,7 @@ velocity.version=1.7
 accessors-smart.version=1.2
 activemq.version=5.15.8
 apache-rat.version=0.13
+apiguardian-api.version=1.1.0
 asm.version=7.1
 bouncycastle.version=1.60
 bsf.version=2.4.0
@@ -83,6 +84,7 @@ junit4.version=4.12
 junit5.version=5.5.1
 log4j.version=2.12.1
 mail.version=1.5.0-b01
+miglayout.version=5.2
 mina-core.version=2.0.19
 mongo-java-driver.version=2.11.3
 neo4j-java-driver.version=1.7.5
diff --git a/src/bom/build.gradle.kts b/src/bom/build.gradle.kts
index cdcec48..96a30cb 100644
--- a/src/bom/build.gradle.kts
+++ b/src/bom/build.gradle.kts
@@ -69,6 +69,8 @@ dependencies {
         apiv("com.helger:ph-commons")
         apiv("com.helger:ph-css")
         apiv("com.jayway.jsonpath:json-path")
+        apiv("com.miglayout:miglayout-core", "miglayout")
+        apiv("com.miglayout:miglayout-swing", "miglayout")
         apiv("com.sun.activation:javax.activation", "javax.activation")
         apiv("com.thoughtworks.xstream:xstream")
         apiv("commons-codec:commons-codec")
@@ -118,6 +120,7 @@ dependencies {
         apiv("org.apache.tika:tika-core", "tika")
         apiv("org.apache.velocity:velocity")
         apiv("org.apache.xmlgraphics:xmlgraphics-commons")
+        apiv("org.apiguardian:apiguardian-api")
         apiv("org.bouncycastle:bcmail-jdk15on", "bouncycastle")
         apiv("org.bouncycastle:bcpkix-jdk15on", "bouncycastle")
         apiv("org.bouncycastle:bcprov-jdk15on", "bouncycastle")
diff --git a/src/components/src/main/java/org/apache/jmeter/visualizers/RespTimeGraphVisualizer.java b/src/components/src/main/java/org/apache/jmeter/visualizers/RespTimeGraphVisualizer.java
index 8e88665..7e620b8 100644
--- a/src/components/src/main/java/org/apache/jmeter/visualizers/RespTimeGraphVisualizer.java
+++ b/src/components/src/main/java/org/apache/jmeter/visualizers/RespTimeGraphVisualizer.java
@@ -522,7 +522,7 @@ public class RespTimeGraphVisualizer extends AbstractVisualizer implements Actio
                 log.error(e.getMessage());
             }
         } else if (eventSource == syncWithName) {
-            graphTitle.setText(namePanel.getName());
+            graphTitle.setText(getName());
         } else if (eventSource == dynamicGraphSize) {
                 enableDynamicGraph(dynamicGraphSize.isSelected());
         } else if (eventSource == samplerSelection) {
diff --git a/src/components/src/main/java/org/apache/jmeter/visualizers/StatGraphVisualizer.java b/src/components/src/main/java/org/apache/jmeter/visualizers/StatGraphVisualizer.java
index 2eb61b9..86d0174 100644
--- a/src/components/src/main/java/org/apache/jmeter/visualizers/StatGraphVisualizer.java
+++ b/src/components/src/main/java/org/apache/jmeter/visualizers/StatGraphVisualizer.java
@@ -748,7 +748,7 @@ public class StatGraphVisualizer extends AbstractVisualizer implements Clearable
                 colorForeGraph = color;
             }
         } else if (eventSource == syncWithName) {
-            graphTitle.setText(namePanel.getName());
+            graphTitle.setText(getName());
         } else if (eventSource == dynamicGraphSize) {
             // if use dynamic graph size is checked, we disable the dimension fields
             if (dynamicGraphSize.isSelected()) {
diff --git a/src/core/build.gradle.kts b/src/core/build.gradle.kts
index 7c4f855..9e578a8 100644
--- a/src/core/build.gradle.kts
+++ b/src/core/build.gradle.kts
@@ -45,6 +45,7 @@ dependencies {
     api("org.apache.logging.log4j:log4j-slf4j-impl") {
         because("Both log4j and slf4j are included, so it makes sense to just add log4j->slf4j bridge as well")
     }
+    api("org.apiguardian:apiguardian-api")
     api("oro:oro") {
         because("Perl5Matcher org.apache.jmeter.util.JMeterUtils.getMatcher()")
     }
@@ -70,6 +71,7 @@ dependencies {
     implementation("com.fasterxml.jackson.core:jackson-annotations")
     implementation("com.fasterxml.jackson.core:jackson-core")
     implementation("com.fasterxml.jackson.core:jackson-databind")
+    implementation("com.miglayout:miglayout-swing")
     implementation("org.freemarker:freemarker")
     implementation("org.mozilla:rhino")
     implementation("org.apache.xmlgraphics:xmlgraphics-commons")
diff --git a/src/core/src/main/java/org/apache/jmeter/control/gui/LoopControlPanel.java b/src/core/src/main/java/org/apache/jmeter/control/gui/LoopControlPanel.java
index 51a3271..082b41c 100644
--- a/src/core/src/main/java/org/apache/jmeter/control/gui/LoopControlPanel.java
+++ b/src/core/src/main/java/org/apache/jmeter/control/gui/LoopControlPanel.java
@@ -33,6 +33,7 @@ import org.apache.jmeter.gui.GUIMenuSortOrder;
 import org.apache.jmeter.gui.util.FocusRequester;
 import org.apache.jmeter.testelement.TestElement;
 import org.apache.jmeter.util.JMeterUtils;
+import org.apiguardian.api.API;
 
 /**
  * The user interface for a controller which specifies that its subcomponents
@@ -42,7 +43,7 @@ import org.apache.jmeter.util.JMeterUtils;
  */
 @GUIMenuSortOrder(3)
 public class LoopControlPanel extends AbstractControllerGui implements ActionListener {
-    private static final long serialVersionUID = 240L;
+    private static final long serialVersionUID = 241L;
 
     /**
      * A checkbox allowing the user to specify whether or not the controller
@@ -56,6 +57,8 @@ public class LoopControlPanel extends AbstractControllerGui implements ActionLis
      */
     private JTextField loops;
 
+    private JLabel loopsLabel;
+
     /**
      * Boolean indicating whether or not this component should display its name.
      * If true, this is a standalone component. If false, this component is
@@ -92,6 +95,21 @@ public class LoopControlPanel extends AbstractControllerGui implements ActionLis
         setState(1);
     }
 
+    @API(status = API.Status.EXPERIMENTAL)
+    public JLabel getLoopsLabel() {
+        return loopsLabel;
+    }
+
+    @API(status = API.Status.EXPERIMENTAL)
+    public JCheckBox getInfinite() {
+        return infinite;
+    }
+
+    @API(status = API.Status.EXPERIMENTAL)
+    public JTextField getLoops() {
+        return loops;
+    }
+
     /**
      * A newly created component can be initialized with the contents of a Test
      * Element object by calling this method. The component is responsible for
@@ -202,7 +220,7 @@ public class LoopControlPanel extends AbstractControllerGui implements ActionLis
         JPanel loopPanel = new JPanel(new BorderLayout(5, 0));
 
         // LOOP LABEL
-        JLabel loopsLabel = new JLabel(JMeterUtils.getResString("iterator_num")); // $NON-NLS-1$
+        loopsLabel = new JLabel(JMeterUtils.getResString("iterator_num")); // $NON-NLS-1$
         loopPanel.add(loopsLabel, BorderLayout.WEST);
 
         JPanel loopSubPanel = new JPanel(new BorderLayout(5, 0));
diff --git a/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java b/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java
index dcd4f56..22416cc 100644
--- a/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java
+++ b/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java
@@ -18,6 +18,11 @@
 
 package org.apache.jmeter.gui;
 
+import static org.apache.jmeter.util.JMeterUtils.labelFor;
+import static org.apiguardian.api.API.Status.DEPRECATED;
+import static org.apiguardian.api.API.Status.EXPERIMENTAL;
+import static org.apiguardian.api.API.Status.INTERNAL;
+
 import java.awt.Component;
 import java.awt.Container;
 import java.awt.Font;
@@ -28,6 +33,8 @@ import javax.swing.JComponent;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
 import javax.swing.border.Border;
 
 import org.apache.jmeter.gui.util.VerticalPanel;
@@ -35,9 +42,12 @@ import org.apache.jmeter.testelement.TestElement;
 import org.apache.jmeter.testelement.property.StringProperty;
 import org.apache.jmeter.util.JMeterUtils;
 import org.apache.jmeter.visualizers.Printable;
+import org.apiguardian.api.API;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import net.miginfocom.swing.MigLayout;
+
 /**
  * This abstract class takes care of the most basic functions necessary to
  * create a viable JMeter GUI component. It extends JPanel and implements
@@ -63,11 +73,16 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
     /** Flag indicating whether or not this component is enabled. */
     private boolean enabled = true;
 
-    /** A GUI panel containing the name of this component. */
+    /**
+     *  A GUI panel containing the name of this component.
+     * @deprecated use {@link #getName()} or {@link AbstractJMeterGuiComponent#createTitleLabel()} for better alignment of the fields
+     **/
+    @API(status = INTERNAL, since = "5.2.0")
+    @Deprecated
+    @SuppressWarnings("DeprecatedIsStillUsed")
     protected NamePanel namePanel;
-    // used by AbstractReportGui
 
-    private final CommentPanel commentPanel;
+    private final JTextArea commentField = new JTextArea();
 
     /**
      * When constructing a new component, this takes care of basic tasks like
@@ -76,8 +91,7 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
      */
     public AbstractJMeterGuiComponent() {
         namePanel = new NamePanel();
-        commentPanel=new CommentPanel();
-        initGui();
+        init();
     }
 
     /**
@@ -97,7 +111,7 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
      *            The comment for the property
      */
     public void setComment(String comment) {
-        commentPanel.setText(comment);
+        commentField.setText(comment);
     }
 
     /**
@@ -125,10 +139,7 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
      */
     @Override
     public String getName() {
-        if (getNamePanel() != null) {
-            return getNamePanel().getName();
-        }
-        return ""; // $NON-NLS-1$
+        return namePanel.getName(); // $NON-NLS-1$
     }
 
     /**
@@ -138,10 +149,7 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
      * @return The comment for the property
      */
     public String getComment() {
-        if (getCommentPanel() != null) {
-            return getCommentPanel().getText();
-        }
-        return ""; // $NON-NLS-1$
+        return commentField.getText();
     }
 
     /**
@@ -151,14 +159,14 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
      * {@link #makeTitlePanel()} instead of directly calling this method.
      *
      * @return a NamePanel containing the name of this component
+     * @deprecated use {@link #getName()} or {@link AbstractJMeterGuiComponent#createTitleLabel()} for better alignment of the fields
      */
+    @API(status = DEPRECATED, since = "5.2.0")
+    @Deprecated
     protected NamePanel getNamePanel() {
         return namePanel;
     }
 
-    private CommentPanel getCommentPanel(){
-        return commentPanel;
-    }
     /**
      * Provides a label containing the title for the component. Subclasses
      * typically place this label at the top of their GUI. The title is set to
@@ -194,7 +202,7 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
     public void configure(TestElement element) {
         setName(element.getName());
         enabled = element.isEnabled();
-        getCommentPanel().setText(element.getComment());
+        commentField.setText(element.getComment());
     }
 
     /**
@@ -209,10 +217,17 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
         enabled = true;
     }
 
-    // helper method - also used by constructor
     private void initGui() {
         setName(getStaticLabel());
-        commentPanel.clearGui();
+        commentField.setText("");
+    }
+
+    private void init() {
+        initGui();
+        // JTextArea does not have border by default (see https://bugs.openjdk.java.net/browse/JDK-4139076)
+        // However we want it to look like a text field. So we borrow a border from there
+        Border border = new JTextField().getBorder();
+        commentField.setBorder(border);
     }
 
     /**
@@ -248,14 +263,29 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete
      * @return a panel containing the component title and name panel
      */
     protected Container makeTitlePanel() {
-        VerticalPanel titlePanel = new VerticalPanel();
-        titlePanel.add(createTitleLabel());
-        VerticalPanel contentPanel = new VerticalPanel();
-        contentPanel.setBorder(BorderFactory.createEmptyBorder());
-        contentPanel.add(getNamePanel());
-        contentPanel.add(getCommentPanel());
-        titlePanel.add(contentPanel);
-        return titlePanel;
+        JPanel titlePanel = new JPanel(new MigLayout("fillx, wrap 2, insets 0", "[][fill,grow]"));
+        titlePanel.add(createTitleLabel(), "span 2");
+
+        JTextField nameField = namePanel.getNameField();
+        titlePanel.add(labelFor(nameField, "name"));
+        titlePanel.add(nameField);
+
+        titlePanel.add(labelFor(nameField, "testplan_comments"));
+        titlePanel.add(commentField);
+
+        // Note: VerticalPanel has a workaround for Box layout which aligns elements, so we can't
+        // use trivial JPanel.
+        // Extra wrapper is often required to ensure that further additions to the panel would be vertical
+        // For instance AbstractVisualizer adds "browse file" panel
+        // If it calls just ..add(browseFilePanel), then it will go to
+        return wrapTitlePanel(titlePanel);
+    }
+
+    @API(status = EXPERIMENTAL, since = "5.2.0")
+    protected Container wrapTitlePanel(Container titlePanel) {
+        VerticalPanel vp = new VerticalPanel();
+        vp.add(titlePanel);
+        return vp;
     }
 
     /**
diff --git a/src/core/src/main/java/org/apache/jmeter/gui/CommentPanel.java b/src/core/src/main/java/org/apache/jmeter/gui/CommentPanel.java
index 160abd1..c69c682 100644
--- a/src/core/src/main/java/org/apache/jmeter/gui/CommentPanel.java
+++ b/src/core/src/main/java/org/apache/jmeter/gui/CommentPanel.java
@@ -18,6 +18,8 @@
 
 package org.apache.jmeter.gui;
 
+import static org.apiguardian.api.API.Status.DEPRECATED;
+
 import java.awt.BorderLayout;
 
 import javax.swing.JLabel;
@@ -27,10 +29,14 @@ import javax.swing.JTextField;
 import javax.swing.border.Border;
 
 import org.apache.jmeter.util.JMeterUtils;
+import org.apiguardian.api.API;
 
 /**
  * Generic comment panel for Test Elements
+ * @deprecated {@link AbstractJMeterGuiComponent#createTitleLabel()} for better alignment of the fields
  */
+@API(status = DEPRECATED, since = "5.2.0")
+@Deprecated
 public class CommentPanel extends JPanel {
     private static final long serialVersionUID = 240L;
 
diff --git a/src/core/src/main/java/org/apache/jmeter/gui/NamePanel.java b/src/core/src/main/java/org/apache/jmeter/gui/NamePanel.java
index 5fb8ea2..5308d43 100644
--- a/src/core/src/main/java/org/apache/jmeter/gui/NamePanel.java
+++ b/src/core/src/main/java/org/apache/jmeter/gui/NamePanel.java
@@ -18,6 +18,8 @@
 
 package org.apache.jmeter.gui;
 
+import static org.apiguardian.api.API.Status.INTERNAL;
+
 import java.awt.BorderLayout;
 import java.util.Collection;
 
@@ -30,6 +32,7 @@ import org.apache.jmeter.testelement.TestElement;
 import org.apache.jmeter.testelement.WorkBench;
 import org.apache.jmeter.testelement.property.StringProperty;
 import org.apache.jmeter.util.JMeterUtils;
+import org.apiguardian.api.API;
 
 public class NamePanel extends JPanel implements JMeterGUIComponent {
     private static final long serialVersionUID = 240L;
@@ -62,6 +65,11 @@ public class NamePanel extends JPanel implements JMeterGUIComponent {
         add(nameField, BorderLayout.CENTER);
     }
 
+    @API(status = INTERNAL, since = "5.2.0")
+    public JTextField getNameField() {
+        return nameField;
+    }
+
     @Override
     public void clearGui() {
         setName(getStaticLabel());
diff --git a/src/core/src/main/java/org/apache/jmeter/threads/gui/AbstractThreadGroupGui.java b/src/core/src/main/java/org/apache/jmeter/threads/gui/AbstractThreadGroupGui.java
index 2dc13b0..2a095c2 100644
--- a/src/core/src/main/java/org/apache/jmeter/threads/gui/AbstractThreadGroupGui.java
+++ b/src/core/src/main/java/org/apache/jmeter/threads/gui/AbstractThreadGroupGui.java
@@ -41,6 +41,8 @@ import org.apache.jmeter.testelement.property.StringProperty;
 import org.apache.jmeter.threads.AbstractThreadGroup;
 import org.apache.jmeter.util.JMeterUtils;
 
+import net.miginfocom.swing.MigLayout;
+
 public abstract class AbstractThreadGroupGui extends AbstractJMeterGuiComponent {
     private static final long serialVersionUID = 240L;
 
@@ -136,7 +138,7 @@ public abstract class AbstractThreadGroupGui extends AbstractJMeterGuiComponent
     }
 
     private JPanel createOnErrorPanel() {
-        JPanel panel = new JPanel();
+        JPanel panel = new JPanel(new MigLayout());
         panel.setBorder(BorderFactory.createTitledBorder(
                 JMeterUtils.getResString("sampler_on_error_action"))); // $NON-NLS-1$
 
diff --git a/src/core/src/main/java/org/apache/jmeter/threads/gui/ThreadGroupGui.java b/src/core/src/main/java/org/apache/jmeter/threads/gui/ThreadGroupGui.java
index 9978565..2098e2c 100644
--- a/src/core/src/main/java/org/apache/jmeter/threads/gui/ThreadGroupGui.java
+++ b/src/core/src/main/java/org/apache/jmeter/threads/gui/ThreadGroupGui.java
@@ -18,30 +18,28 @@
 
 package org.apache.jmeter.threads.gui;
 
+import static org.apache.jmeter.util.JMeterUtils.labelFor;
+
 import java.awt.BorderLayout;
 import java.awt.event.ItemEvent;
 import java.awt.event.ItemListener;
 
 import javax.swing.BorderFactory;
-import javax.swing.ButtonGroup;
-import javax.swing.ImageIcon;
 import javax.swing.JCheckBox;
 import javax.swing.JLabel;
 import javax.swing.JPanel;
-import javax.swing.JRadioButton;
 import javax.swing.JTextField;
-import javax.swing.SwingConstants;
 
 import org.apache.jmeter.control.LoopController;
 import org.apache.jmeter.control.gui.LoopControlPanel;
-import org.apache.jmeter.gui.util.HorizontalPanel;
-import org.apache.jmeter.gui.util.VerticalPanel;
 import org.apache.jmeter.testelement.TestElement;
 import org.apache.jmeter.testelement.property.BooleanProperty;
 import org.apache.jmeter.threads.AbstractThreadGroup;
 import org.apache.jmeter.threads.ThreadGroup;
 import org.apache.jmeter.util.JMeterUtils;
 
+import net.miginfocom.swing.MigLayout;
+
 public class ThreadGroupGui extends AbstractThreadGroupGui implements ItemListener {
     private static final long serialVersionUID = 240L;
 
@@ -51,23 +49,24 @@ public class ThreadGroupGui extends AbstractThreadGroupGui implements ItemListen
 
     private static final String RAMP_NAME = "Ramp Up Field";
 
-    private JTextField threadInput;
+    private final JTextField threadInput = new JTextField();
 
-    private JTextField rampInput;
+    private final JTextField rampInput = new JTextField();
 
     private final boolean showDelayedStart;
 
     private JCheckBox delayedStart;
 
-    private JCheckBox scheduler;
-
-    private JTextField duration;
+    private final JCheckBox scheduler = new JCheckBox(JMeterUtils.getResString("scheduler"));
 
-    private JTextField delay; // Relative start-up time
+    private final JTextField duration = new JTextField();
+    private final JLabel durationLabel = labelFor(duration, "duration");
 
-    private JRadioButton sameUserBox;
+    private final JTextField delay = new JTextField(); // Relative start-up time
+    private final JLabel delayLabel = labelFor(delay, "delay");
 
-    private JRadioButton differentUserBox;
+    private final JCheckBox sameUserBox =
+            new JCheckBox(JMeterUtils.getResString("threadgroup_same_user"));
 
     public ThreadGroupGui() {
         this(true);
@@ -141,7 +140,9 @@ public class ThreadGroupGui extends AbstractThreadGroupGui implements ItemListen
      */
     private void toggleSchedulerFields(boolean enable) {
         duration.setEnabled(enable);
+        durationLabel.setEnabled(enable);
         delay.setEnabled(enable);
+        delayLabel.setEnabled(enable);
     }
 
     private JPanel createControllerPanel() {
@@ -153,34 +154,6 @@ public class ThreadGroupGui extends AbstractThreadGroupGui implements ItemListen
     }
 
 
-    /**
-     * Create a panel containing the Duration field and corresponding label.
-     *
-     * @return a GUI panel containing the Duration field
-     */
-    private JPanel createDurationPanel() {
-        JPanel panel = new JPanel(new BorderLayout(5, 0));
-        JLabel label = new JLabel(JMeterUtils.getResString("duration")); // $NON-NLS-1$
-        panel.add(label, BorderLayout.WEST);
-        duration = new JTextField();
-        panel.add(duration, BorderLayout.CENTER);
-        return panel;
-    }
-
-    /**
-     * Create a panel containing the Duration field and corresponding label.
-     *
-     * @return a GUI panel containing the Duration field
-     */
-    private JPanel createDelayPanel() {
-        JPanel panel = new JPanel(new BorderLayout(5, 0));
-        JLabel label = new JLabel(JMeterUtils.getResString("delay")); // $NON-NLS-1$
-        panel.add(label, BorderLayout.WEST);
-        delay = new JTextField();
-        panel.add(delay, BorderLayout.CENTER);
-        return panel;
-    }
-
     @Override
     public String getLabelResource() {
         return "threadgroup"; // $NON-NLS-1$
@@ -204,77 +177,43 @@ public class ThreadGroupGui extends AbstractThreadGroupGui implements ItemListen
         delay.setText(""); // $NON-NLS-1$
         duration.setText(""); // $NON-NLS-1$
         sameUserBox.setSelected(true);
-        differentUserBox.setSelected(false);
     }
 
-   private void init() { // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
+    private void init() { // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
         // THREAD PROPERTIES
-        VerticalPanel threadPropsPanel = new VerticalPanel();
+        JPanel threadPropsPanel = new JPanel(new MigLayout("fillx, wrap 2", "[][fill,grow]"));
         threadPropsPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),
                 JMeterUtils.getResString("thread_properties"))); // $NON-NLS-1$
 
         // NUMBER OF THREADS
-        JPanel threadPanel = new JPanel(new BorderLayout(5, 0));
-
-        JLabel threadLabel = new JLabel(JMeterUtils.getResString("number_of_threads")); // $NON-NLS-1$
-        threadPanel.add(threadLabel, BorderLayout.WEST);
-
-        threadInput = new JTextField(5);
+        threadPropsPanel.add(labelFor(threadInput, "number_of_threads")); // $NON-NLS-1$
         threadInput.setName(THREAD_NAME);
-        threadLabel.setLabelFor(threadInput);
-        threadPanel.add(threadInput, BorderLayout.CENTER);
-
-        threadPropsPanel.add(threadPanel);
+        threadPropsPanel.add(threadInput);
 
         // RAMP-UP
-        JPanel rampPanel = new JPanel(new BorderLayout(5, 0));
-        JLabel rampLabel = new JLabel(JMeterUtils.getResString("ramp_up")); // $NON-NLS-1$
-        rampPanel.add(rampLabel, BorderLayout.WEST);
-
-        rampInput = new JTextField(5);
+        threadPropsPanel.add(labelFor(rampInput, "ramp_up"));
         rampInput.setName(RAMP_NAME);
-        rampLabel.setLabelFor(rampInput);
-        rampPanel.add(rampInput, BorderLayout.CENTER);
-
-        threadPropsPanel.add(rampPanel);
+        threadPropsPanel.add(rampInput);
 
         // LOOP COUNT
-        threadPropsPanel.add(createControllerPanel());
-        threadPropsPanel.add(createUserOptionsPanel());
+        LoopControlPanel loopController = (LoopControlPanel) createControllerPanel();
+        threadPropsPanel.add(loopController.getLoopsLabel(), "split 2");
+        threadPropsPanel.add(loopController.getInfinite(), "gapleft push");
+        threadPropsPanel.add(loopController.getLoops());
+        threadPropsPanel.add(sameUserBox, "span 2");
         if (showDelayedStart) {
             delayedStart = new JCheckBox(JMeterUtils.getResString("delayed_start")); // $NON-NLS-1$
-            threadPropsPanel.add(delayedStart);
+            threadPropsPanel.add(delayedStart, "span 2");
         }
-        scheduler = new JCheckBox(JMeterUtils.getResString("scheduler")); // $NON-NLS-1$
         scheduler.addItemListener(this);
-        threadPropsPanel.add(scheduler);
-        VerticalPanel mainPanel = new VerticalPanel();
-        mainPanel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),
-                JMeterUtils.getResString("scheduler_configuration"))); // $NON-NLS-1$
-
-        ImageIcon warningImg = JMeterUtils.getImage("warning.png");
-        JLabel warningLabel = new JLabel(JMeterUtils.getResString("thread_group_scheduler_warning"),
-                warningImg, SwingConstants.CENTER); // $NON-NLS-1$
-        mainPanel.add(warningLabel);
-        mainPanel.add(createDurationPanel());
-        mainPanel.add(createDelayPanel());
-        toggleSchedulerFields(false);
-        VerticalPanel intgrationPanel = new VerticalPanel();
-        intgrationPanel.add(threadPropsPanel);
-        intgrationPanel.add(mainPanel);
-        add(intgrationPanel, BorderLayout.CENTER);
+
+        threadPropsPanel.add(scheduler, "span 2");
+
+        threadPropsPanel.add(durationLabel);
+        threadPropsPanel.add(duration);
+        threadPropsPanel.add(delayLabel);
+        threadPropsPanel.add(delay);
+        add(threadPropsPanel, BorderLayout.CENTER);
     }
 
-   private JPanel createUserOptionsPanel(){
-       ButtonGroup group = new ButtonGroup();
-       sameUserBox = new JRadioButton(JMeterUtils.getResString("threadgroup_same_user")); //$NON-NLS-1$
-       group.add(sameUserBox);
-       sameUserBox.setSelected(true);
-       differentUserBox = new JRadioButton(JMeterUtils.getResString("threadgroup_different_user")); //$NON-NLS-1$
-       group.add(differentUserBox);
-       JPanel optionsPanel = new HorizontalPanel();
-       optionsPanel.add(sameUserBox);
-       optionsPanel.add(differentUserBox);
-       return optionsPanel;
-   }
 }
diff --git a/src/core/src/main/java/org/apache/jmeter/util/JMeterUtils.java b/src/core/src/main/java/org/apache/jmeter/util/JMeterUtils.java
index bdf26fe..d5a657b 100644
--- a/src/core/src/main/java/org/apache/jmeter/util/JMeterUtils.java
+++ b/src/core/src/main/java/org/apache/jmeter/util/JMeterUtils.java
@@ -18,6 +18,7 @@
 
 package org.apache.jmeter.util;
 
+import java.awt.Component;
 import java.awt.Dialog;
 import java.awt.Font;
 import java.awt.Frame;
@@ -49,6 +50,7 @@ import java.util.concurrent.ThreadLocalRandom;
 import java.util.stream.Collectors;
 
 import javax.swing.ImageIcon;
+import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 import javax.swing.JScrollPane;
 import javax.swing.JTable;
@@ -915,6 +917,19 @@ public class JMeterUtils implements UnitTestManager {
     }
 
     /**
+     * Creates {@link JLabel} that is associated with a given {@link Component} instance.
+     * @param component component for the label
+     * @param resourceId resource ID to be used for retrieving label text
+     * @return JLabel instance
+     */
+    public static JLabel labelFor(Component component, String resourceId) {
+        JLabel label = new JLabel(getResString(resourceId));
+        label.setName(resourceId);
+        label.setLabelFor(component);
+        return label;
+    }
+
+    /**
      * Takes an array of strings and a tokenizer character, and returns a string
      * of all the strings concatenated with the tokenizer string in between each
      * one.
diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
index 1bc0b75..628abbe 100644
--- a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
@@ -484,7 +484,7 @@ include_controller=Include Controller
 include_equals=Include Equals?
 include_path=Include Test Plan
 increment=Increment
-infinite=Forever
+infinite=Infinite
 initial_context_factory=Initial Context Factory
 insert_after=Insert After
 insert_before=Insert Before
@@ -872,7 +872,7 @@ proxy_test_plan_creation=Test Plan Creation
 proxy_test_plan_filtering=Requests Filtering
 proxy_title=HTTP(S) Test Script Recorder
 pt_br=Portuguese (Brazilian)
-ramp_up=Ramp-Up Period (in seconds)\:
+ramp_up=Ramp-up period (seconds)\:
 random_control_title=Random Controller
 random_order_control_title=Random Order Controller
 random_multi_result_source_variable=Source Variable(s) (use | as separator)
@@ -1072,7 +1072,7 @@ save_timestamp=Save Time Stamp
 save_url=Save URL
 save_workbench=Save WorkBench
 sbind=Single bind/unbind
-scheduler=Scheduler
+scheduler=Specify Thread lifetime
 scheduler_configuration=Scheduler Configuration
 schematic_view_errors=Error on generation of schematic view
 schematic_view_generation_ok=Successful generation of schematic view in {0}
diff --git a/src/licenses/build.gradle.kts b/src/licenses/build.gradle.kts
index cf21f61..3367032 100644
--- a/src/licenses/build.gradle.kts
+++ b/src/licenses/build.gradle.kts
@@ -90,6 +90,14 @@ val gatherBinaryLicenses by tasks.registering(GatherLicenseTask::class) {
         expectedLicense = SpdxLicense.BSD_2_Clause
     }
 
+    for (mig in listOf("com.miglayout:miglayout-core", "com.miglayout:miglayout-swing")) {
+        overrideLicense(mig) {
+            expectedLicense = SimpleLicense("BSD", uri("http://www.debian.org/misc/bsd.license"))
+            effectiveLicense = SpdxLicense.BSD_3_Clause
+            licenseFiles = "miglayout"
+        }
+    }
+
     overrideLicense("com.thoughtworks.xstream:xstream:1.4.11") {
         expectedLicense = SimpleLicense("BSD style", uri("http://x-stream.github.io/license.html"))
         // https://github.com/x-stream/xstream/issues/151
diff --git a/src/licenses/licenses/miglayout/LICENSE b/src/licenses/licenses/miglayout/LICENSE
new file mode 100644
index 0000000..5a0ea8a
--- /dev/null
+++ b/src/licenses/licenses/miglayout/LICENSE
@@ -0,0 +1,27 @@
+ License (BSD):
+ ==============
+
+ Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (miglayout (at) miginfocom (dot) com)
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+ Redistributions of source code must retain the above copyright notice, this list
+ of conditions and the following disclaimer.
+ 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.
+ Neither the name of the MiG InfoCom AB 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 OWNER 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.
diff --git a/xdocs/images/screenshots/setup_thread_group.png b/xdocs/images/screenshots/setup_thread_group.png
index 7750c24..85f191a 100644
Binary files a/xdocs/images/screenshots/setup_thread_group.png and b/xdocs/images/screenshots/setup_thread_group.png differ
diff --git a/xdocs/images/screenshots/teardown_thread_group.png b/xdocs/images/screenshots/teardown_thread_group.png
index d75b326..29afa43 100644
Binary files a/xdocs/images/screenshots/teardown_thread_group.png and b/xdocs/images/screenshots/teardown_thread_group.png differ
diff --git a/xdocs/images/screenshots/thread_group_distributed.png b/xdocs/images/screenshots/thread_group_distributed.png
index 5e1ad12..eec161b 100644
Binary files a/xdocs/images/screenshots/thread_group_distributed.png and b/xdocs/images/screenshots/thread_group_distributed.png differ
diff --git a/xdocs/images/screenshots/threadgroup.png b/xdocs/images/screenshots/threadgroup.png
index f0d3d2d..e20f8a7 100644
Binary files a/xdocs/images/screenshots/threadgroup.png and b/xdocs/images/screenshots/threadgroup.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 5fa5c32..956b1f3 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -6445,16 +6445,16 @@ Behaviour can be modified with some properties by setting in user.properties:
         </property>
         <property name="Number of Threads" required="Yes">Number of users to simulate.</property>
         <property name="Ramp-up Period" required="Yes">How long JMeter should take to get all the threads started.  If there are 10 threads and a ramp-up time of 100 seconds, then each thread will begin 10 seconds after the previous thread started, for a total time of 100 seconds to get the test fully up to speed.</property>
-        <property name="Loop Count" required="Yes, unless forever is selected">Number of times to perform the test case.  Alternatively, "<code>forever</code>" can be selected causing the test to run until manually stopped.</property>
+        <property name="Loop Count" required="Yes, unless Infinite is selected">Number of times to perform the test case.  Alternatively, "<code>infinite</code>" can be selected causing the test to run until manually stopped or end of the thread lifetime is reached.</property>
         <property name="Delay Thread creation until needed" required="Yes">
         If selected, threads are created only when the appropriate proportion of the ramp-up time has elapsed.
         This is most appropriate for tests with a ramp-up time that is significantly longer than the time to execute a single thread.
-        I.e. where earlier threads finish before later ones start. 
+        I.e. where earlier threads finish before later ones start.
         <br></br>
         If not selected, all threads are created when the test starts (they then pause for the appropriate proportion of the ramp-up time).
         This is the original default, and is appropriate for tests where threads are active throughout most of the test.
         </property>
-        <property name="Scheduler" required="Yes">If selected, enables the scheduler</property>
+        <property name="Specify Thread lifetime" required="Yes">If selected, confines Thread operation time to the given bounds</property>
         <property name="Duration (seconds)" required="No">
             If the scheduler checkbox is selected, one can choose a relative end time. 
             JMeter will use this to calculate the End Time.
@@ -7054,7 +7054,7 @@ This is done by default since JMeter 2.13.
 </note>
 </component>
 
-<component name="setUp Thread Group" index="&sect-num;.9.10" width="908" height="364" screenshot="setup_thread_group.png">
+<component name="setUp Thread Group" index="&sect-num;.9.10" width="1252" height="828" screenshot="setup_thread_group.png">
 <description>
     <p>
     A special type of ThreadGroup that can be utilized to perform Pre-Test Actions.  The behavior of these threads
@@ -7064,7 +7064,7 @@ This is done by default since JMeter 2.13.
 </description>
 </component>
 
-<component name="tearDown Thread Group" index="&sect-num;.9.11" width="909" height="366" screenshot="teardown_thread_group.png">
+<component name="tearDown Thread Group" index="&sect-num;.9.11" width="1248" height="824" screenshot="teardown_thread_group.png">
 <description>
     <p>
     A special type of ThreadGroup that can be utilized to perform Post-Test Actions.  The behavior of these threads