Jump to content

My Advice on GUIs


liverare

Recommended Posts

It would be more beneficial for you to know how to hard code GUIs before reading this.

 

I enjoy creating GUIs. It's challenging, because there's quite a bit to consider in order to get them just right!
 
Friendly UX (User Experience)
Either your users are dumb, or the way you've constructed your GUI is misleading and/or has contradictory decisions (e.g., 'power-mine' and 'bank' check boxes).
 
Secure
As mentioned above with the contradictory decisions: never allow them. Be strict and firm with the freedom's you're giving to your users, so they don't end up making mistakes.
 
But also make sure the user cannot NOT make decisions, for instance, prevent the bot from working until your users have told your bot what it needs to know in order to work in the first place.
 
Externalise
GUIs included in the main script code will make the code look ugly and bulky, because GUI code is always ugly and bulky. Having your GUI code saved in a separate class (aka. code saved in a separate file) and loaded in will make your code look a hell of a lot better!
 
OOP
Following on from the previous point; there's various ways to have information sent from your GUI to your script, but the best (in my opinion) is to use OOP.
 
 
 
 
Example:

1_zpsma059ide.png

  • I have not focused on UI (User Interface), because this is just proof-of-concept.
  • The box to the top is a list inside of a scroll pane (when list becomes bigger than box, scroll pane is used to allow scrolling).
  • The text field underneath is where you type monster names and you can separate them with a comma.
  • Notice the space between the 'add remove' buttons and the check box below? That's to hint that those buttons relate to the list above and not the stuff below.
  • Notice the '25' up-down box (aka. spinner) to the bottom-right is disabled; that's because 'Run away when health falls below' is disabled. This is part of UX -- prevent options from being interacted if they're ultimately pointless to the user.
  • Most importantly: the 'Submit' (aka. "start", "go", etc.) button is DISABLED until the list has names inside of it, otherwise what the hell is my bot going to do? ...

2_zpskk975oo7.png

  • The names I had entered were 'trimmed' to avoid spaces before the name (e.g., " GUARD"), and I have capitalised all the names so that case sensitivity is not a concern for the user, and the list cannot be populated with crazy-case names (e.g., "GuaRD"). It looks cleaner.
  • I have enabled that spinner for the check box, because the check box is selected.
  • The submit button is now enabled, because the bot actually has something to work with.

However there are more UX features that are working behind the scene!

  • You can select multiple items in that list and hit [backspace] or [delete] and they will be removed. When doing this, the next list item to be selected will be the one before the one you deleted, to make it easier to delete multiple items just by using your keyboard.
  • When entering names into the text field, when you hit [enter] the names are added to the list.
  • When adding names (either by [enter] or 'add'), the text field is cleared.
  • You cannot enter the same name twice into the list; this removes unnecessary duplicates.

Here's the code:
 
SomeScript.java (Script)
Look at the loop() method: the bot will not do anything until the user is finished with the GUI.

import java.util.concurrent.atomic.AtomicReference;

import javax.swing.SwingUtilities;

import org.osbot.rs07.script.Script;
import org.osbot.rs07.script.ScriptManifest;

import gui.Display;
import gui.Instruction;

@ScriptManifest(author = "LiveRare", info = "", logo = "", name = "Some Random Script GUI Example", version = 9000)
public class SomeScript extends Script {

	Display gui;
	
	/*
	 * User AtomicReference because the GUI runs concurrently to the script,
	 * which means multi-threading and the problems that arise when two lines of
	 * code attempt to execute at the same time.
	 * 
	 * But also, if you have a 'wrapper' class that can be shared amongst all of
	 * your script classes, then you can have all those classes access the one
	 * (and only one) instance of the Instruction event.
	 */
	AtomicReference<Instruction> instructionRef;

	@Override
	public void onStart() throws InterruptedException {
		
		instructionRef = new AtomicReference<>();
		
		SwingUtilities.invokeLater(() -> {
			gui = new Display();
			gui.setSubmitListener((e) -> instructionRef.set(e));
			gui.setVisible(true);
		});
		
	}
	
	@Override
	public int onLoop() throws InterruptedException {
		
		Instruction i = instructionRef.get();
		
		if (i != null) {
		
			log(i.getNames());
			log(i.isRunAway() + " @ " + i.getRunAwayAmount());
			
			// With the user input, do something with it
			// TODO: loop through name list and kill anything with that name 
		
		} else {
			
			// Still waiting on user input. We can rotate camera or other stuff here to avoid being logged out.
			
			if (gui != null && !gui.isVisible()) {
				
				// User has closed the GUI before making any decisions, so we can assume the user's not wanting to run our script
				stop(false);
			}
			
		}
		
		return 2500;
	}
	
	@Override
	public void onExit() throws InterruptedException {
		
		if (gui != null) { // check just encase...
			gui.dispose();
		}
	}

} 


 
Display.java (GUI)

import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Collections;
import java.util.List;

import javax.swing.DefaultListModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.GroupLayout.Alignment;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;

/**
 * 
 * @author LiveRare
 *
 */
public class Display extends JFrame {
	
	private SubmitListener submitListener;

	public Display() {
		
		initComponents();
	}
	
	public SubmitListener getSubmitListener() {
		return submitListener;
	}
	
	public void setSubmitListener(SubmitListener submitListener) {
		this.submitListener = submitListener;
	}
	
	
	/*
	 * Initialise components and configure layout, etc.
	 */
	
	public void initComponents() {
		
		// Buttons
		btnAddName = new JButton("Add");
		btnRemoveName = new JButton("Remove");
		btnSubmit = new JButton("Submit");
		btnSubmit.setEnabled(false); // Only enable button if we have names in our list

		// List model (must come first before use!)
		model = new DefaultListModel<>();
		
		// List (adding list model)
		lstNames = new JList<>(model);
		
		// Scroll panes
		scrListNames = new JScrollPane(lstNames);
		
		// Text field
		txtName = new JTextField(20);
		
		// Check box
		chkRunAway = new JCheckBox("Run away when health falls below");
		
		// Spinner
		spnRunAway = new JSpinner(new SpinnerNumberModel(25, 1, 99, 5));
		spnRunAway.setEnabled(false); // Disable as, by default, check box will be unselected
		
		// This
		super.setTitle("GUI Example - Target NPC");
		super.setSize(512, 512);
		super.setResizable(false);
		super.setLocationRelativeTo(getOwner());
		super.setDefaultCloseOperation(HIDE_ON_CLOSE);
		
		// This -> add (done in layout)
		GroupLayout layout = new GroupLayout(super.getContentPane());
		
		layout.setAutoCreateContainerGaps(true);
		layout.setAutoCreateGaps(true);
		
		// Horizontal layout
		layout.setHorizontalGroup(layout.createParallelGroup(Alignment.CENTER)
				.addComponent(scrListNames)
				.addComponent(txtName)
				.addGroup(layout.createSequentialGroup()
						.addComponent(btnAddName)
						.addGap(10, 15, 20)
						.addComponent(btnRemoveName))
				.addGroup(layout.createSequentialGroup()
						.addComponent(chkRunAway)
						.addComponent(spnRunAway))
				.addComponent(btnSubmit));
		
		// Vertical layout
		layout.setVerticalGroup(layout.createSequentialGroup()
				.addComponent(scrListNames)
				.addComponent(txtName)
				.addGroup(layout.createParallelGroup()
						.addComponent(btnAddName)
						.addComponent(btnRemoveName))
				.addGap(10, 15, 20)
				.addGroup(layout.createParallelGroup()
						.addComponent(chkRunAway)
						.addComponent(spnRunAway))
				.addComponent(btnSubmit));
		
		super.setLayout(layout);
		
		pack();
		
		// Listeners
		
		// Add listener to check box for UX
		chkRunAway.addActionListener((e) -> spnRunAway.setEnabled(chkRunAway.isSelected()));
		
		// Add listener to add button, and text field (for UX)
		ActionListener addName = ((e) -> addNames());
		btnAddName.addActionListener(addName);
		txtName.addActionListener(addName);
		
		// Add listener to remove button
		ActionListener removeNames = ((e) -> removeNames());
		btnRemoveName.addActionListener(removeNames);
		
		lstNames.registerKeyboardAction(removeNames, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
				JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
		lstNames.registerKeyboardAction(removeNames, KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0),
				JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
		
		model.addListDataListener(new ListDataListener() { // Not a "functional interface" aka. has more than one method in it, so no Lambda sweetness 
			
			@Override
			public void intervalRemoved(ListDataEvent e) {
				// Set button enabled if there still exists names in the list
				btnSubmit.setEnabled(!model.isEmpty());
			}
			
			@Override
			public void intervalAdded(ListDataEvent e) {
				// By default we can enable submit (because we're adding stuff)
				btnSubmit.setEnabled(true);
			}
			
			@Override
			public void contentsChanged(ListDataEvent e) {
			}
		});
		
		// THE MOST IMPORTANT LISTENER
		btnSubmit.addActionListener((e) -> {
			if (submitListener != null) { // We're checking to see if we have submit listener on start
				submitListener.submitRecieved( // We're calling back an interface with a new event of our own ("instruction")
						new Instruction( // We're creating instruction event with captured information from the gui
								Collections.list(model.elements()),
								chkRunAway.isSelected(),
								(Integer) spnRunAway.getValue()));
			}
		});
	}
	
	/*
	 * Methods
	 */
	
	private void addNames() {

		String input = txtName.getText();
		
		if (input != null && !input.isEmpty()) {
			
			String[] names = input.toUpperCase().split(",");
			
			for (String name : names) {
				
				name = name.trim(); // Trim dat shit
				
				if (!name.isEmpty() && !model.contains(name)) {

					model.addElement(name);
				}
			}
			
		}
		
		txtName.setText("");
		
	}

	private void removeNames() {

		int selectedIndex = lstNames.getSelectedIndex();

		List<String> selectedNames = lstNames.getSelectedValuesList();

		selectedNames.forEach(name -> model.removeElement(name));

		// Just changing next selected list item for better UX
		if (!model.isEmpty() && selectedIndex > 1) {

			lstNames.setSelectedIndex(selectedIndex - 1);
		}
	}
	
	
	/*
	 * Component variables
	 */
	
	private JButton btnAddName;
	private JButton btnRemoveName;
	private JButton btnSubmit;
	private JList<String> lstNames;
	private DefaultListModel<String> model;
	private JScrollPane scrListNames;
	private JTextField txtName;
	private JCheckBox chkRunAway;
	private JSpinner spnRunAway;

	/*
	 * Test
	 */
	
	public static void main(String[] arg0) {
		new Display().setVisible(true);
	}
}
 


 
Instruction.java (Object)
This object contains the cut-and-dry of the GUI based on the user's choices. Having the information from the GUI stored this way not only makes it easier to handle in our script, but also means we can use APIs such as JSonSimple to extend this object and make it possible to save the user's decisions locally, and then have the script load them at the user's behest.

import java.util.List;

/**
 * 
 * @author LiveRare
 *
 */
public class Instruction {

	private final List<String> names;
	private final boolean runAway;
	private final int runAwayAmount;

	public Instruction(List<String> names, boolean runAway, int runAwayAmount) {
		this.names = names;
		this.runAway = runAway;
		this.runAwayAmount = runAwayAmount;
	}

	public List<String> getNames() {
		return names;
	}

	public boolean isRunAway() {
		return runAway;
	}

	public int getRunAwayAmount() {
		return runAwayAmount;
	}

}
 


 
SubmitListener.java (Interface)
In order for our Script to receive the user's instructions from the GUI we need some kind of method that allows for that communication. So what we do is we have this SubmitListener as a variable in the GUI, then we add some public getters/setters to allow the Script to set the SubmitListener inside of the GUI, then the 'submit' button in the GUI will then callback the script using this variable IF this variable has been set.

/**
 * 
 * @author LiveRare
 *
 */
public interface SubmitListener {

	void submitRecieved(Instruction e);
	
} 

 


 
With all this the Script can create the GUI, add the necessary interface to allow the GUI to callback to the script when the user's made their decision, then to dispose of the GUI when the script has stopped. To me, this makes the GUI feels more natural.

 

 

 

 

 

This is just a rough draft; may improve thread later.

Edited by liverare
  • Like 9
Link to comment
Share on other sites

 

Just gonna post an example of a bad GUI at my own expense...

337e81c2e06cd25f26f8a42b62561774.png

  • The entire top section is confusing as fuck.
  • "Food name (for bank)" sounds very weird and can confuse some users.
  • "Enable banking" is also a bit weird

The whole "Antipattern Settings" section is even worse.

 

tl;dr: don't make a gui like mine

 
 
 
  • Like 4
Link to comment
Share on other sites

tbh i always read ur name as liver - are but now im just seeing that it is LiveRare (when u typed like that), nice tutorial though.

 

Yeah I've heard that a lot.

 

I did have it as "Live Rare", but some usernames wouldn't accept spaces, and then I just got into the habit of not having any spaces. :)

  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...