liverare Posted December 12, 2015 Share Posted December 12, 2015 (edited) 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). SecureAs 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. ExternaliseGUIs 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! OOPFollowing 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: 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? ... 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 December 12, 2015 by liverare 9 Quote Link to comment Share on other sites More sharing options...
Bobrocket Posted December 12, 2015 Share Posted December 12, 2015 Just gonna post an example of a bad GUI at my own expense... 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 4 Quote Link to comment Share on other sites More sharing options...
blm95 Posted December 12, 2015 Share Posted December 12, 2015 Nice advice, will look to implement this stuff in my scripts; it's pretty pathetic that so many silly checks have to be implemented solely because of the incompetency of some users lol... Quote Link to comment Share on other sites More sharing options...
Dark Magician Posted December 12, 2015 Share Posted December 12, 2015 (edited) This is very interesting and should be beneficial for our users here on OSBot... if implemented correctly. Edited December 12, 2015 by Dark Magician Quote Link to comment Share on other sites More sharing options...
Deceiver Posted December 12, 2015 Share Posted December 12, 2015 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. Quote Link to comment Share on other sites More sharing options...
liverare Posted December 12, 2015 Author Share Posted December 12, 2015 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. 1 Quote Link to comment Share on other sites More sharing options...