-2

I know that something similar has been asked before on this thread here: Access JLabel from another class However when I tried using the "this.lblNewLabel = new JLabel();" for all my JLabels in my code but it didn't work. My problem is that I am trying to get the JLabels from "MainFrame" to be accessed by "bananaBreadIngredients" and by "ServingsButtons" so that the action Listener from "ServingsButtons" can work on the components in "bananaBreadIngredients" and I need help trying to get that work. If their is anything that doesn't make sure I'll try to answer any questions about my code that I can.

Here is my main JFrame code (this is the code where I thought would be best to put the JLabels as I thought that they would be able to be accessed by other classes like the "bananaBreadIngredients" class):

package sweets;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

public class MainFrame extends JFrame implements ItemListener {
    //all the foods
    public int servingNumber = 0;
    //ingredient Volumes
    public int almondFlourVol = 0;
    public int allPurposeFlourVol = 0;
    public int unsaltedButterVol = 0;
    public int bakingSodaVol = 0;
    public int bakingPowderVol = 0;
    public int vanillaExtractVol = 0;
    public int cocoaPowderVol = 0;
    public int saltVol = 0;
    public int chocolateChipsVol = 0;
    public int instantEspressoVol = 0;
    public int vegetableOilVol = 0;
    public int bananasVol = 0;
    //sugars
    public int granulatedSugarVol = 0;
    public int powderedSugarVol = 0;
    public int lightBrownSugarVol = 0;
    //eggs
    public int eggsVol = 0;
    public int eggWhitesVol = 0;

    JLabel granulatedSugar = new JLabel("Granulated Sugar: " + (granulatedSugarVol));
    JLabel powderedSugar = new JLabel("Powdered Sugar: " + (powderedSugarVol));
    JLabel lightBrownSugar = new JLabel("Light Brown Sugar: " + (lightBrownSugarVol));
    JLabel eggs = new JLabel("Eggs: " + (eggsVol));
    JLabel eggWhites = new JLabel("Egg Whites: " + (eggWhitesVol));
    JLabel bananas = new JLabel("Bananas: " + (bananasVol));
    JLabel vegetableOil = new JLabel("Vegetable Oil: " + (vegetableOilVol));
    JLabel instantEspresso = new JLabel("Instant Espresso: " + (instantEspressoVol));
    JLabel chocolateChips = new JLabel("Chocolate Chips: " + (chocolateChipsVol));
    JLabel salt = new JLabel("Salt: " + (saltVol));
    JLabel cocoaPowder = new JLabel("Cocoa Powder: " + (cocoaPowderVol));
    JLabel vanillaExtract = new JLabel("Vanilla Extract: " + (vanillaExtractVol));
    JLabel bakingSoda = new JLabel("Baking Soda: " + (bakingSodaVol));
    JLabel unsaltedButter = new JLabel("Unsalted Butter: " + (unsaltedButterVol));
    JLabel allPurposeFlour = new JLabel("All-Purpose Flour: " + (allPurposeFlourVol));
    JLabel almondFlour = new JLabel("Almond Flour: " + (almondFlourVol));
    JLabel bakingPowder = new JLabel("Baking Powder: " + (bakingPowderVol));

    String[] food = {bananaBread, brownies, chocolateChipcookies, macarons};
    final static String bananaBread = "banana bread";
    final static String brownies = "brownies";
    final static String chocolateChipcookies = "chocolate chip cookies";
    final static String macarons = "macarons";
    JPanel ingredients;

    private ServingButtons servingButtonsJPanel;
    private bananaBreadIngredients bananaBreadIngredientsJPanel;

    JPanel browniesIngredients = new JPanel(new BorderLayout());
    JPanel cookieIngredients = new JPanel(new BorderLayout());
    JPanel macaronIngredients = new JPanel(new BorderLayout());

    public MainFrame(){
        JPanel recipeName = new JPanel(); //this will have the JComboBox with the foodList
        JLabel recipe = new JLabel("Recipes: ");
        recipeName.setLayout(new FlowLayout());
        recipeName.add(recipe);
        JComboBox foodList = new JComboBox(food);
        foodList.setEditable(false);
        foodList.addItemListener(this);
        ingredients = new JPanel(new CardLayout());
        //MainFrame.ComboBoxRenderer renderer= new MainFrame.ComboBoxRenderer();
        getContentPane().add(foodList, BorderLayout.NORTH);
        recipeName.add(foodList);

        //Servings buttons
        servingButtonsJPanel = new ServingButtons(); //<<-- method call
        servingButtonsJPanel.setLayout(new GridLayout(1,1));
        //sources used from this: https://stackoverflow.com/questions/29477242/jpanel-and-custom-class-extending-jpanel-not-displaying-correctly



        JLabel ingredientLabel = new JLabel("Ingredients Go here: ");
        bananaBreadIngredientsJPanel = new bananaBreadIngredients();
        ingredients.add(ingredientLabel);
        //only works if you add the cards in the same order that they are put in in the array
        ingredients.add(bananaBreadIngredientsJPanel, bananaBread );
        ingredients.add(browniesIngredients, brownies);
        ingredients.add(cookieIngredients, chocolateChipcookies);
        ingredients.add(macaronIngredients, macarons);
        getContentPane().add(ingredients, BorderLayout.CENTER);

        //this will have the dynamically changing ingredients (placeholder: the code for this in the mainIA class but it is not being used as I want to implement it through a class instead of having it in the constructor class like in the mainIA)
        JLabel inputRecipe = new JLabel("Input Recipe"); //this will have the input recipe JTextField (placeholder)


        this.setTitle("Recipe Portion Calculator");
        this.setLayout(new GridLayout(4,1));
        this.setSize(800,800);
        this.add(recipeName);
        this.add(servingButtonsJPanel); //<<-- issue is that I have a GUI in the ServingButtons class but when I call it in the MainFrame class it does not show up in the GUI
        this.add(ingredients);
        this.add(inputRecipe);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        this.setVisible(true);
    }

    public void itemStateChanged(ItemEvent evt) {
        CardLayout cl = (CardLayout)(ingredients.getLayout());
        cl.show(ingredients, (String)evt.getItem());
    }
}

Here is the class for ServingsButtons. This class has the increment action listener that is applied to the different JLabels:

package sweets;

import javax.swing.*;
import java.awt.*;

public class ServingButtons extends JPanel {
    public int servingNumber = 0;
    //ingredient Volumes

    JButton incButton = new JButton("+");
    JButton decButton = new JButton(("-"));

    public ServingButtons() {
        super();
        this.setLayout(new GridLayout(1, 4));
        //used this code from this Quora Link: https://www.quora.com/How-would-I-get-my-program-to-add-1-every-time-I-press-the-button-%E2%80%9Da%E2%80%9D-in-Java
        this.add(new JLabel("Number of Servings: "));
        JLabel counterLbl = new JLabel(Integer.toString(servingNumber));
        decButton.addActionListener(l -> {
            counterLbl.setText(Integer.toString(--servingNumber));
            almondFlour.setText("Almound Flour: " + (--almondFlourVol));
            granulatedSugar.setText("Granulated Sugar: " + (--granulatedSugarVol));
            powderedSugar.setText("Powdered Sugar: " + (--powderedSugarVol));
            lightBrownSugar.setText("Light Brown Sugar: " +(--lightBrownSugarVol));
            eggs.setText("Eggs: " + (--eggsVol));
            eggWhites.setText("Egg Whites: " + (--eggWhitesVol));
            bananas.setText("Bananas: " + (--bananasVol));
            vegetableOil.setText("Vegetable Oil: " + (--vegetableOilVol));
            instantEspresso.setText("Instant Espresso: " + (--instantEspressoVol));
            chocolateChips.setText("Chocolate Chips: " + (--chocolateChipsVol));
            salt.setText("Salt: " + (--saltVol));
            cocoaPowder.setText("Cocoa Powder: " + (--cocoaPowderVol));
            vanillaExtract.setText("Vanilla Extract: " + (--vanillaExtractVol));
            bakingSoda.setText("Baking Soda: " + (--bakingSodaVol));
            unsaltedButter.setText("Unsalted Butter: " + (--unsaltedButterVol));
            allPurposeFlour.setText("All-Purpose Flour: " + (--allPurposeFlourVol));
            bakingPowder.setText("All-Purpose Flour: " + (--bakingPowderVol));
        });
        JLabel space = new JLabel("");
        this.add(space);
        this.add(decButton);
        this.add(counterLbl);
        incButton.addActionListener(l -> {
            counterLbl.setText(Integer.toString(++servingNumber));
            granulatedSugar.setText("Granulated Sugar: " + (++granulatedSugarVol));
            powderedSugar.setText("Powdered Sugar: " + (++powderedSugarVol));
            lightBrownSugar.setText("Light Brown Sugar: " + (++lightBrownSugarVol));
            eggs.setText("Eggs: " + (++eggsVol));
            eggWhites.setText("Egg Whites: " + (++eggWhitesVol));
            bananas.setText("bananas: " + (++bananasVol));
            vegetableOil.setText("Vegetable Oil: "+ (++vegetableOilVol));
            instantEspresso.setText("instant Espresso: " + (++instantEspressoVol));
            chocolateChips.setText("Chocolate Chips: "+ (++chocolateChipsVol));
            salt.setText("Salt: " + (++saltVol));
            cocoaPowder.setText("Cocoa Powder: " + (++cocoaPowderVol));
            vanillaExtract.setText("Vanilla Extract: " + (++vanillaExtractVol));
            bakingSoda.setText("Baking Soda: " + (++bakingSodaVol));
            unsaltedButter.setText("Unsalted Buttter: " + (++unsaltedButterVol));
            allPurposeFlour.setText("All-Purpose Flour: " + (++allPurposeFlourVol));
            almondFlour.setText("Almound Flour: " + (++almondFlourVol));
            bakingPowder.setText("All-Purpose Flour: " + (++bakingPowderVol));
        });
        this.add(incButton);
    }
}

here is the code for the class that I want to have access to the JLabels so that the code from "ServingsButtons" applies to the code on this class:

package sweets;

import javax.swing.*;
import java.awt.*;

public class bananaBreadIngredients extends JPanel {
    public bananaBreadIngredients(){
        super();
        this.setLayout(new GridLayout(9,1));
        this.add(bananas);
        this.add(unsaltedButter);
        this.add(granulatedSugar);
        this.add(allPurposeFlour);
        this.add(bakingSoda);
        this.add(salt);
        this.add(vanillaExtract);
    }
}

Any help would be much appreciated. Thankyou!

  • 2
    *public class bananaBreadIngredients extends JPanel* You could make these classes private inner classes. Alternatively, the way to access variables in another class is via.. accessors (getters) Btw, class names begin [upper case](https://technojeeves.com/index.php/aliasjava1/106-java-style-conventions) – g00se Jul 29 '23 at 22:21
  • One thing you should consider is “responsibility”, I mean, who’s actually responsible for managing the labels? In most cases you want restrict this to the defining class, as it could also be managing multiple states that need to taken into consideration. So what do you do? You use an observer pattern that would allow your second class to notify your first that some state has changed. This concept further supports dependency injection and the model-view-controller concept – MadProgrammer Jul 29 '23 at 23:52
  • 2
    Honestly, I think you need to take a step back and not focus on the UI. Start be describing the elements of your code, you have a `Recipe` which has `Ingredients`, each ingredient can have a "measure" or "quality" associated with it. These elements are describing your data/model. Around this, you then need to build your UI to support these concepts – MadProgrammer Jul 30 '23 at 00:41
  • hey @MadProgrammer. Thanks for the input. Would an example of me describing elements of code be like this: "MainFrame which extends JFrame holds the main constructor class to create the code for all the components in the GUI."? Sorry if I misunderstood your comment or what you meant by describing the elements of my code. – Nicholas McNamara Jul 30 '23 at 21:06
  • @NicholasMcNamara no, what they are trying to say is that the model you are using to represent your data is extremely disorganized, and therefore, any GUI you build on top of it will be painful to work with and difficult. For example, a lot of your previous posts on this code are getting tripped up on GUI details, but those details are made so much more complex and difficult to solve because your backend is disheveled. – davidalayachew Jul 30 '23 at 22:31
  • Take a look at the answer that @HovercraftFullOfEels is crafting for you. That is an idea of a clean backend. Try and rework your code to follow a similar style, and I'm sure that these problems you have been having will be much easier to navigate/deal with. – davidalayachew Jul 30 '23 at 22:33
  • @NicholasMcNamara No, describe a "recipe" <- This is acting as your "data" or "model" – MadProgrammer Jul 31 '23 at 01:14

3 Answers3

3

First things first: Before even thinking of a GUI program, you must think about what the GUI is supposed to represent, what it is displaying, and this information should first be created as object-oriented compliant classes. For example, say you wanted to create a GUI that displayed recipes, then applying the rule above, we'd first want to create classes that represented a single recipe, one that we can call, perhaps, "Recipe.java". This could contain a String name field, a List of Strings to represent directions, and a list of ingredients. Ingredients could also be Strings, but if you wanted to be able to fiddle with the measures, best to create an Ingredients class, one with a name, string for a measuring unit, and a number for measured amount.

These could look something like:

Ingredient.java

public class Ingredient {
    private String name;
    private String measureUnit;
    private int measureAmount;

    public Ingredient(String name, String measureUnit, int measureAmount) {
        this.name = name;
        this.measureUnit = measureUnit;
        this.measureAmount = measureAmount;
    }

    public String getName() {
        return name;
    }

    public String getMeasureUnit() {
        return measureUnit;
    }

    public int getMeasureAmount() {
        return measureAmount;
    }

    public int getMeasureAmount(int servingsMultiplier) {
        return servingsMultiplier * measureAmount;
    }

    public void setMeasureAmount(int measureAmount) {
        this.measureAmount = measureAmount;
    }

    @Override
    public String toString() {
        return "Ingredient [name=" + name + "]";
    }

}

Recipe.java

import java.util.List;

public class Recipe {
    private String name;
    private List<Ingredient> ingredients;
    private List<String> directions;
    private int servingsMultiplier = 1;

    public Recipe(String name, List<String> directions) {
        this.name = name;
        this.directions = directions;
    }

    public int getIngredientCount() {
        return ingredients.size();
    }

    public Recipe(String name, List<String> directions, List<Ingredient> ingredients) {
        this(name, directions);
        this.ingredients = ingredients;
    }

    public String getName() {
        return name;
    }

    public List<String> getDirections() {
        return directions;
    }
    
    public void addDirection(String dir) {
        directions.add(dir);
    }
    
    public void addDirection(int index, String dir) {
        directions.add(index, dir);
    }
    
    public void removeDirection(String dir) {
        directions.remove(dir);
    }

    public List<Ingredient> getIngredients() {
        return ingredients;
    }

    public void addIngredient(Ingredient ingredient) {
        ingredients.add(ingredient);
    }

    public int getMultiplier() {
        return servingsMultiplier;
    }

    public void setMultiplier(int servingsMultiplier) {
        // can never be less than zero
        this.servingsMultiplier = Math.max(0, servingsMultiplier);
    }

    @Override
    public String toString() {
        return "Recipe [name=" + name + ", ingredients=" + ingredients + ", directions=" + directions + "]";
    }

}

Only after first working this out, should you then start to create a GUI.

Now with these guys created, you can create a GUI that works with multiple different recipes, not just one hard-coded recipe.

Next, I would like to think about how to store recipe information in a file, a file that can be retrieved and used to create Recipe objects in the GUI.

There are several ways of handling information "persistance", this includes the Java Serialization library, but this results in a non-text binary file, one that can only be read and written to by the Java program, and this is not a wise way to represent textual information.

So, instead let's consider "textual serialization", there are several ways to do this, including writing everything to text using toString() methods, and this results in easy to create code for the writing part, but not so easy when it comes to reading the data (deserialization). A few years ago, I'd recommend using Java's XML libraries, but more recently, I have been using JSON which requires outside libraries, but which is easier to use with different programming languages, including JavaScript. I like both the GSON and Jackson libraries, but for this simple app, will stick with GSON, which to me seems a little easier.

So if we wrote a Recipe object to file using GSON, like so:

RecipeIO.java

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class RecipeIO {
    public static void writeRecipe(String fileName, Recipe recipe) throws IOException {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        Path path = Paths.get(fileName);
        String text = gson.toJson(recipe);
        Files.writeString(path, text, StandardOpenOption.CREATE);
    }

    public static Recipe recipeFromGsonFile(String fileName) throws IOException {
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String recipeJson = Files.readString(Paths.get(fileName));
        Recipe recipe = gson.fromJson(recipeJson, Recipe.class);
        return recipe;
    }
}

It could create a text file that looked something like this:

{
  "name": "Omelet Recipe",
  "ingredients": [
    {
      "name": "Egg",
      "measureUnit": "eggs",
      "measureAmount": 3
    },
    {
      "name": "Onion",
      "measureUnit": "onions",
      "measureAmount": 1
    },
    {
      "name": "Garlic",
      "measureUnit": "cloves",
      "measureAmount": 4
    },
    {
      "name": "Butter",
      "measureUnit": "tablespoons",
      "measureAmount": 2
    },
    {
      "name": "Diced Ham",
      "measureUnit": "ounces",
      "measureAmount": 2
    },
    {
      "name": "Shredded Cheese",
      "measureUnit": "ounces",
      "measureAmount": 2
    },
    {
      "name": "Milk",
      "measureUnit": "tablespoons",
      "measureAmount": 2
    }
  ],
  "directions": [
    "Dice the onions and the garlic.",
    "Crack the eggs into a mixing bowl and add the milk. ",
    "Whisk the eggs and milk together until well combined. ",
    "Season with a pinch of salt and pepper according to your taste.",
    "Heat a non-stick frying pan over medium heat. Add the butter or cooking oil and let it melt or heat up.",
    "Once the butter is melted and the pan is hot, add the diced onions and garlic and saute until clear.",
    "Next, pour the whisked eggs into the pan, making sure they spread evenly.",
    "Let the eggs cook for a minute or two until they start to set at the edges.",
    "Sprinkle the diced ham evenly over the eggs.",
    "Add the shredded cheese on top of the ham.",
    "With a spatula, carefully lift one side of the omelet and fold it over the other side, covering the ham and cheese filling. This will create a half-moon shape.",
    "Cook for another minute or so until the cheese is melted, and the omelet is cooked through. ",
    "You can adjust the cooking time to achieve your desired level of doneness.",
    "Carefully slide the omelet onto a plate and serve hot."
  ],
  "servingsMultiplier": 1
}

This is pretty straightforward to read and understand, and it can be edited in a plain text editor, and without difficulty.


Some working GUI code, explanation (and improvement) to follow:

RecipeMain.java

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.io.IOException;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import javax.swing.text.PlainDocument;

public class RecipeMain {
    private static final String FILE_NAME = "recipe01.txt";

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            Recipe omelet = null;

            try {
                omelet = RecipeIO.recipeFromGsonFile(FILE_NAME);
            } catch (IOException e) {
                e.printStackTrace();
            }

            RecipeGuiMainPanel mainPanel = new RecipeGuiMainPanel(omelet);

            JFrame frame = new JFrame("Recipe");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(mainPanel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

@SuppressWarnings("serial")
class RecipeGuiMainPanel extends JPanel {
    private Recipe recipe;
    private RecipePanel recipePanel;
    private DirectionsPanel directionsPanel;
    private ServingsPanel servingsPanel = new ServingsPanel();

    public RecipeGuiMainPanel(Recipe recipe) {

        // TODO: put this into a "controller"
        servingsPanel.addChangeListener(cl -> servingsChanged());

        JLabel titleLabel = new JLabel(recipe.getName(), SwingConstants.CENTER);
        titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 24f));
        JPanel titlePanel = new JPanel();
        titlePanel.add(titleLabel);

        JLabel directionsLabel = new JLabel("Directions", SwingConstants.CENTER);
        directionsLabel.setFont(directionsLabel.getFont().deriveFont(Font.BOLD, 18f));
        JPanel directionsLabelPanel = new JPanel();
        directionsLabelPanel.add(directionsLabel);

        directionsPanel = new DirectionsPanel(recipe);

        this.recipe = recipe;
        recipePanel = new RecipePanel(recipe);
        setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
        add(titlePanel);
        add(recipePanel);
        add(directionsLabelPanel);
        add(directionsPanel);
        add(servingsPanel);

        servingsChanged();
    }

    private void servingsChanged() {
        int servingsMultiplier = servingsPanel.getMultiplierValue();
        recipePanel.setServingsMultiplier(servingsMultiplier);
    }

    public Recipe getRecipe() {
        return recipe;
    }
}

@SuppressWarnings("serial")
class RecipePanel extends JPanel {
    private Recipe recipe;
    private RecipeIngredientsTableModel tableModel;
    private RecipeTable ingredientsTable;

    public RecipePanel(Recipe recipe) {
        this.recipe = recipe;
        tableModel = new RecipeIngredientsTableModel(recipe);

        ingredientsTable = new RecipeTable(tableModel);
        JScrollPane ingredientsScrollPane = new JScrollPane(ingredientsTable);
        ingredientsScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        setLayout(new BorderLayout());
        add(ingredientsScrollPane);
    }

    public void setServingsMultiplier(int servingsMultiplier) {
        tableModel.setServingsMultiplier(servingsMultiplier);
    }

    public Recipe getRecipe() {
        return recipe;
    }

    public void setRecipe(Recipe recipe) {
        this.recipe = recipe;
    }

}

@SuppressWarnings("serial")
class DirectionsPanel extends JPanel {
    private static final int VISIBLE_ROW_COUNT = 5;
    private DefaultListModel<String> dirListModel = new DefaultListModel<>();
    private JList<String> directionsList = new JList<>(dirListModel);
    private Recipe recipe;

    public DirectionsPanel(Recipe recipe) {
        this.recipe = recipe;
        directionsList.setVisibleRowCount(VISIBLE_ROW_COUNT);
        // directionsList.setCellRenderer(new CustomRenderer(2, 20));
        // directionsList.setCellRenderer(new MyCellRenderer());
        directionsList.setCellRenderer(new CustomRenderer());
        JScrollPane scrollPane = new JScrollPane(directionsList);

        for (String direction : recipe.getDirections()) {
            dirListModel.addElement(direction);
        }

        setLayout(new BorderLayout());
        add(scrollPane);
    }

    public Recipe getRecipe() {
        return recipe;
    }

    // borrowing some ideas from Andrew Thompson's code
    // link: https://stackoverflow.com/a/7306536/
    @SuppressWarnings("unused")
    private class CustomRenderer implements ListCellRenderer<String> {
        private static final int LABEL_WIDTH = 300;
        private JLabel label;

        public CustomRenderer() {
            label = new JLabel();
            int bc = 220;
            Color borderColor = new Color(bc, bc, bc);
            label.setBorder(BorderFactory.createLineBorder(borderColor));
            label.setFont(label.getFont().deriveFont(Font.BOLD));
        }

        @Override
        public Component getListCellRendererComponent(JList<? extends String> list, String value, int index,
                boolean isSelected, boolean cellHasFocus) {
            int width = list.getWidth();
            String pre = String.format("<html><body style='width: %dpx;'> ", LABEL_WIDTH);
            System.out.println("width: " + pre);
            String text = pre + " " + '\u2022' + "  " + value;
            label.setText(text);
            return label;
        }
    }

}

@SuppressWarnings("serial")
class RecipeTable extends JTable {
    private static final int MAX_HEIGHT = 150;

    public RecipeTable(TableModel tm) {
        super(tm);
    }

    @Override
    public Dimension getPreferredScrollableViewportSize() {
        Dimension superSize = super.getPreferredScrollableViewportSize();
        int width = superSize.width;
        int height = Math.min(superSize.height, MAX_HEIGHT);

        return new Dimension(width, height);
    }
}

@SuppressWarnings("serial")
class RecipeIngredientsTableModel extends AbstractTableModel {
    private static final String[] COL_HEADERS = { "Name", "Unit", "Amount" };
    private Recipe recipe;

    public RecipeIngredientsTableModel(Recipe recipe) {
        this.recipe = recipe;
    }

    public void setServingsMultiplier(int servingsMultiplier) {
        recipe.setMultiplier(servingsMultiplier);
        fireTableDataChanged();
    }

    @Override
    public int getRowCount() {
        return recipe.getIngredients().size();
    }

    @Override
    public int getColumnCount() {
        return COL_HEADERS.length;
    }

    public String getColumnName(int column) {
        return COL_HEADERS[column];
    };

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Ingredient ingredient = recipe.getIngredients().get(rowIndex);
        switch (columnIndex) {
        case 0:
            return ingredient.getName();
        case 1:
            return ingredient.getMeasureUnit();
        case 2:
            return ingredient.getMeasureAmount() * recipe.getMultiplier();

        default:
            String message = String.format("Recipe column index invalid (row, col): [%d, %d]", rowIndex, columnIndex);
            throw new ArrayIndexOutOfBoundsException(message);
        }
    }

    public java.lang.Class<?> getColumnClass(int columnIndex) {
        switch (columnIndex) {
        case 0:
        case 1:
            return String.class;
        case 2:
            return Integer.class;

        default:
            String message = String.format("Recipe column index invalid (row, col): [%d]", columnIndex);
            throw new ArrayIndexOutOfBoundsException(message);
        }
    }

    public void addIngredient(Ingredient ingredient) {
        recipe.addIngredient(ingredient);
        int row = recipe.getIngredientCount() - 1;
        fireTableRowsInserted(row, row);
    }

}

@SuppressWarnings("serial")
class ServingsPanel extends JPanel {
    private static final int STARTING_VALUE = 1;
    private static final int MIN = 0;
    private static final int MAX = 10;
    private static final int STEP = 1;

    private SpinnerModel model = new SpinnerNumberModel(STARTING_VALUE, MIN, MAX, STEP);
    private JSpinner servingsSpinner = new JSpinner(model);

    public ServingsPanel() {
        setBorder(BorderFactory.createTitledBorder("Number of Servings"));
        add(servingsSpinner);
    }

    public void addChangeListener(ChangeListener changeListener) {
        model.addChangeListener(changeListener);
    }

    public int getMultiplierValue() {
        return (int) model.getValue();
    }
}

I decided to create another, simpler example, one that directly addresses your primary question, that of changing a JLabel from another JPanel. I wanted to show a simpler GUI program, one with a model, view and controller, the key concepts being:

  • The model holds the data of the program as well as its fundamental behaviors (methods).
  • The view is the GUI display that shows to the user the state of the model, and allows the user the ability to change this state through GUI events
  • The controller, which is the glue that holds both model and view together.

Key also:

  • The model should know nothing about the view, meaning, it should be created in a way that would allow it to work with almost any type of view, including a simple console program, a Swing GUI, a JavaFX GUI, an android program, some future user interface yet to be created, ...
  • The view should be relatively "dumb". It should show things and respond to input, but it should off-load the logic to the controller and the view.
  • Key is separating the model from the view, and I cannot stress this enough, to have "low coupling" between the two.
  • Object oriented fundamentals are much more important than the structure of the view. If you mess up the former in a large complex program, and then have to go back and make corrections and changes, you're in for a world of pain. If on the other hand your model's OOP is well-created, and yet you have to fix GUI bugs, that is usually not as difficult. A metaphor could be, it is easier to fix a house's roof, than it is its foundation.

So the program, starting with the model:

CountModel.java

This holds a count int variable and a List of ChangeListeners. These listeners are added by outside classes that want to listen for changes to the model. All the listener does is to notify all the listeners via a stateChanged(...) method call whenever the model's value changes. When this happens, the listeners can then get the new result by calling getCount() on this model

import java.util.ArrayList;
import java.util.List;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class CountModel {
    private int count;
    private List<ChangeListener> changeListeners = new ArrayList<>();

    public CountModel() {
        count = 0;
    }

    public int getCount() {
        return count;
    }

    public void incrementCount() {
        count++;
        notifyListeners();
    }

    public void decrementCount() {
        count--;
        // can't be below 0
        count = Math.max(count, 0);
        notifyListeners();
    }

    private void notifyListeners() {
        ChangeEvent changeEvent = new ChangeEvent(this);
        for (ChangeListener changeListener : changeListeners) {
            changeListener.stateChanged(changeEvent);
        }
    }

    public void addChangeListener(ChangeListener listener) {
        changeListeners.add(listener);
    }

}

CountDisplayPanel.java

This class displays the count value in a JLabel. Note that that is all that this panel does, nothing more nothing less. It does not get input from the user, nothing. An outside class can update its count value by calling the setCount(...) method which receives an int, it then formats the int into a 2-digit number String that is then displayed in the countLabel.

import java.awt.BorderLayout;
import java.awt.Font;
import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;

@SuppressWarnings("serial")
public class CountDisplayPanel extends JPanel {
    private JLabel countLabel = new JLabel("00", SwingConstants.CENTER);

    public CountDisplayPanel() {
        countLabel.setFont(new Font(Font.DIALOG, Font.BOLD, 48));
        int gap = 30;
        countLabel.setBorder(BorderFactory.createEmptyBorder(gap / 2, gap, gap / 2, gap));

        setLayout(new BorderLayout());
        add(countLabel);
        add(new JLabel("Count", SwingConstants.CENTER), BorderLayout.PAGE_START);
    }

    public void setCount(int count) {
        String text = String.format("%02d", count);
        countLabel.setText(text);
    }
}

IncrementDecrementPanel.java

This panel has two buttons, an increment button and a decrement button, that don't do anything by themselves. An outside class can add ActionListeners to the buttons so that they can be notified of these events via the two addXxxxListener(...) methods.

import java.awt.GridLayout;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class IncrementDecrementPanel extends JPanel {
    private JButton incrementButton = new JButton("Increment");
    private JButton decrementButton = new JButton("Decrement");

    public IncrementDecrementPanel() {
        incrementButton.setMnemonic(KeyEvent.VK_I);
        decrementButton.setMnemonic(KeyEvent.VK_D);
        int gap = 5;
        setBorder(BorderFactory.createEmptyBorder(gap, gap, gap, gap));
        setLayout(new GridLayout(1, 0, gap, gap));
        add(incrementButton);
        add(decrementButton);
    }

    public void addIncrementListener(ActionListener listener) {
        incrementButton.addActionListener(listener);
    }

    public void addDecrementListener(ActionListener listener) {
        decrementButton.addActionListener(listener);
    }
}

CountView.java

This panel holds/displays the two JPanels above and represents the main model. It has no brains of its own -- it does nothing internally to respond to the incr or decr buttons, and it does not directly change the CountDisplayPanel's JLabel. What it instead does is delegate the key methods of the two sub JPanels to expose them, to allow the controller to be notified and to make changes

import java.awt.BorderLayout;
import java.awt.event.ActionListener;

import javax.swing.JPanel;

@SuppressWarnings("serial")
public class CountView extends JPanel {
    private CountDisplayPanel countDisplayPanel = new CountDisplayPanel();
    private IncrementDecrementPanel incrDecrPanel = new IncrementDecrementPanel();

    public CountView() {
        setLayout(new BorderLayout());
        add(countDisplayPanel);
        add(incrDecrPanel, BorderLayout.PAGE_END);
    }

    // delegating methods through to the enclosed objects
    public void setCount(int count) {
        countDisplayPanel.setCount(count);
    }
    
    public void addIncrementListener(ActionListener listener) {
        incrDecrPanel.addIncrementListener(listener);
    }

    public void addDecrementListener(ActionListener listener) {
        incrDecrPanel.addDecrementListener(listener);
    }

}

CountController.java

This class accepts a CountView object and a CountModel object and allows one to change the other. It adds a change listener to the model, so that if the model's state changes (the count int variable takes a new value), then the countChanged() method is called which extracts the count value, and then places it into the view. This class also attaches action listeners to the incr and decr JButtons so that it is notified of their presses. When this occurs, the corresponding incrementCount() or decrementCount() method will be callse, and these methods will then notify the view that it needs to change its state. So information is going through this class from the view into th model, when a button is pressed, and from the model to the view, when the count is changed.

public class CountController {
    CountView view;
    CountModel model;

    public CountController(CountView view, CountModel model) {
        this.view = view;
        this.model = model;

        model.addChangeListener(e -> countChanged());
        
        view.addIncrementListener(e -> incrementCount());
        view.addDecrementListener(e -> decrementCount());
    }

    private void countChanged() {
        int count = model.getCount();
        view.setCount(count);
    }
    
    private void incrementCount() {
        model.incrementCount();
    }
    
    private void decrementCount() {
        model.decrementCount();
    }

}

SimpleExample.java

The program that puts it all together. Here we create our model and view, we create the controller, passing in the model and the view, and then create a JFrame to display the view.

import javax.swing.*;

public class SimpleExample {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            CountModel model = new CountModel();
            CountView view = new CountView();
            new CountController(view, model);

            JFrame frame = new JFrame("Count Example");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(view);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • Ohh, didn't see this before posting +1!! – MadProgrammer Jul 31 '23 at 01:06
  • 1
    @MadProgrammer: that's fine. There is no such thing as "too many answers". I sort of cheated, as I have not fully finished my answer. I think that we agree on an a few fundamentals, that one key is in creating a clean object-oriented model, that one must try to isolate the view from the model as much as possible, to achieve low coupling, and that to jump into GUI while ignoring fundamental issues is a grave mistake. 1+ to your answer, of course. – Hovercraft Full Of Eels Jul 31 '23 at 01:33
  • Hey @HovercraftFullOfEels, first of all I just wanted to say thank you very much for going out of your way to help me. I understand the underlying concepts of MVC programing in JAVA however I still had a question about my own program, that being, should I create an MVC for every major aspect of my program? For example in my program, I would like to have a JCombobox that when the different options in the combo box are clicked different recipes with ingredients appear. Would/Should I create a whole new set of MVC for that one function of my program? – Nicholas McNamara Aug 08 '23 at 22:03
2

The "simple" answer to your question could be answered by reading (and understanding) Passing Information to a Method or a Constructor however, you seem to be digging yourself into a hole which isn't helping you achieve what you seem to want to achieve.

Important, the following isn't a "answer" per se, but more of a directional change which will, in the long run, help you achieve the things you want.

The "Data"

The first thing you want to do is seperate the "date" from the "interface". The data should drive the UI, not the other way round. You also need to beware, it's the UI's responsibility to "display" the data, so your data should avoid providing "display" information (like toString).

Lets start with some very basic concepts. You have a recipe, a recipe has a number of measurements of an ingredient.

For convince, recipes could be grouped into "books".

These concepts might look something like this...

public enum Unit {
    CUP, MILLI_LETER, EACH, GRAMS, TEA_SPOON
}

public interface Measurement {
    public double getQuantity();
    public Unit getUnit();
}

public interface Ingredient {
    public String getName();
    public Measurement getMeasurement();

    public static String ALMOND_FLOUR = "Almond Flour";
    public static String ALL_PURPOSE_FLOUR = "All Purpose Flour";
    public static String UNSALTED_BUTTER = "Unsalted Butter";
    public static String BAKING_SODA = "Baking Soda";
    public static String BAKING_POWDER = "Baking Powder";
    public static String VANILLA_EXTRACT = "Vanilla Extract";
    public static String COCOA_POWDER = "Cocoa Powder";
    public static String SALT = "Salt";
    public static String CHOCOLATE_CHIPS = "Chocolate Chips";
    public static String INSTANT_ESPRESSO = "Instant Espresso";
    public static String VEGETABLE_OIL = "Vegetable Oil";
    public static String BANANAS = "Bananas";
    public static String GRANULATED_SUGAR = "Granulated Sugar";
    public static String POWDERED_SUGAR = "Powdered Sugar";
    public static String LIGHT_BROWN_SUGAR = "Light Brown Sugar";
    public static String EGG = "Egg";
    public static String EGG_WHITE = "Egg White";
}

public interface Recipe {
    public String getName();
    public Ingredient[] getIngredients();
}

public interface RecipeBook {
    public String getName();
    public Recipe[] getRecipes();
}

The use of enum for the Unit will restrict the acceptable units. You might find you need more units and you'd expand the concept here.

You should note a unit might be represented differently, for example "liter" and "milli-liters". Personally, I'd avoid having both and instead choice one and then have some kind of converter/formatter which could better render the result, for example 0.5 liters would be displayed as 500 mls - this is a very important point. The way the data is stored and the way the data is displayed should be independent from each other and it's not the responsibility of the data to dictate presentation.

Now, the above makes use of interfaces which describe functionality. This supports the Hide Implementation Details principle.

Your code should not care how a Recipe is implemented, only that it provides the functionality described by the interface. This is often termed as a "contract".

This means that a Recipe could be build manually, or come from a database or web service, but the code that uses it simply doesn't care.

But, we will need some implementations, so should start with some "default" implementations for our needs...

public class DefaultMeasurement implements Measurement {
    private double quantity;
    private Unit unit;

    public DefaultMeasurement(double quantity, Unit unit) {
        this.quantity = quantity;
        this.unit = unit;
    }

    @Override
    public double getQuantity() {
        return quantity;
    }

    @Override
    public Unit getUnit() {
        return unit;
    }

}

public class DefaultIngredient implements Ingredient {
    private String name;
    private Measurement measurement;

    public DefaultIngredient(String name, Measurement measurement) {
        this.name = name;
        this.measurement = measurement;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Measurement getMeasurement() {
        return measurement;
    }
}

public class DefaultRecipe implements Recipe {
    private String name;
    private Ingredient[] ingredients;

    public DefaultRecipe(String name, Ingredient[] ingredients) {
        this.name = name;
        this.ingredients = ingredients;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Ingredient[] getIngredients() {
        return ingredients;
    }
}

public class DefaultRecipeBook implements RecipeBook {
    private String name;
    private Recipe[] recipes;

    public DefaultRecipeBook(String name, Recipe[] recipes) {
        this.name = name;
        this.recipes = recipes;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Recipe[] getRecipes() {
        return recipes;
    }
}

These aren't anything special, but do provide us with a starting point.

Up till now, we've not done any UI work. In fact, you could take the above code and make a console app out of them or they could form the bases for web service or web app, they are decoupled, making them, re-usable!

Now, we need to start making some decisions. For example, we could build up some default recipes and books, maybe doing something like...

public class RecipeBuilder {
    private String name;
    private List<Ingredient> ingredients = new ArrayList<>(8);

    public RecipeBuilder(String name) {
        this.name = name;
    }

    public RecipeBuilder with(Ingredient ingredient) {
        getIngredients().add(ingredient);
        return this;
    }

    protected String getName() {
        return name;
    }

    protected List<Ingredient> getIngredients() {
        return ingredients;
    }

    public Recipe build() {
        List<Ingredient> ingredients = getIngredients();
        return new DefaultRecipe(getName(), ingredients.toArray(new Ingredient[ingredients.size()]));
    }
}

public class GrandmasRecipeBook extends DefaultRecipeBook {
    public GrandmasRecipeBook() {
        super("Grandmas",
                new Recipe[]{
                    new RecipeBuilder("Banana Bread")
                            .with(new DefaultIngredient(Ingredient.BANANAS, new DefaultMeasurement(6, Unit.EACH)))
                            .with(new DefaultIngredient(Ingredient.UNSALTED_BUTTER, new DefaultMeasurement(30, Unit.GRAMS)))
                            .with(new DefaultIngredient(Ingredient.GRANULATED_SUGAR, new DefaultMeasurement(0.75, Unit.CUP)))
                            .with(new DefaultIngredient(Ingredient.ALL_PURPOSE_FLOUR, new DefaultMeasurement(1, Unit.CUP)))
                            .with(new DefaultIngredient(Ingredient.BAKING_SODA, new DefaultMeasurement(1, Unit.TEA_SPOON)))
                            .with(new DefaultIngredient(Ingredient.SALT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
                            .with(new DefaultIngredient(Ingredient.VANILLA_EXTRACT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
                            .build()
                });
    }
}

Here, we've used a builder pattern to make it easier to build up a concept of a Recipe, again, it's de-coupled in the fact that the underlying implementation of Recipe is unknown to the caller ... and the caller doesn't care.

I will note, however, this does contradict the Composition Over Inheritance principle, so you need to beware, sometimes doing this can be considered an anti-pattern, so, instead, I'll do something different.

Making it pretty (ie the UI)

Up until till now, we've not touched the UI at all, let's take a look at how that might work...

public class RecipeLibraryPane extends JPanel {
    protected enum View {
        RECIPE_BOOKS,
        RECIPE_BOOK_CONTENTS,
        RECIPE,
    }
    private CardLayout cardLayout;

    private RecipesListPane recipesListPane;
    private RecipeBookPane recipeBookPane;
    private RecipePane recipePane;

    public RecipeLibraryPane(RecipeBook[] books) {
        cardLayout = new CardLayout();

        recipesListPane = new RecipesListPane(
                books,
                new RecipesListPane.Observer() {
            @Override
            public void didSelectRecipeBook(RecipesListPane source, RecipeBook book) {
                recipeBookPane.setRecipeBook(book);
                show(View.RECIPE_BOOK_CONTENTS);
            }
        });

        recipeBookPane = new RecipeBookPane(new RecipeBookPane.Observer() {
            @Override
            public void didSelectRecipe(RecipeBookPane source, Recipe recipe) {
                recipePane.setRecipe(recipe);
                show(View.RECIPE);
            }

            @Override
            public void didNavigateBack(RecipeBookPane source) {
                show(View.RECIPE_BOOKS);
            }
        });
        
        recipePane = new RecipePane(
                new RecipePane.Observer() {
            @Override
            public void didNavigateBack(RecipePane source) {
                show(View.RECIPE_BOOK_CONTENTS);
            }
        });

        setLayout(cardLayout);
        add(recipesListPane, View.RECIPE_BOOKS);
        add(recipeBookPane, View.RECIPE_BOOK_CONTENTS);
        add(recipePane, View.RECIPE);

        show(View.RECIPE_BOOKS);
    }

    protected void add(Component comp, View view) {
        add(comp, view.name());
    }

    protected void show(View view) {
        cardLayout.show(this, view.name());
    }
}

This is the primary UI. It manages the navigation between the different views. I've used a enum (and some convenience methods) to manage the views because I will ALWAYS mistype the view names .

Also note, the list of books is passed to the UI, supporting dependency injection

// Encapsulate some of the re-usable workflows
public class LabelFactory {
    public enum Style {
        TITLE,
        SUB_TITLE,
        DEFAULT;
    }
    private String text;
    private Style style;

    public LabelFactory(String text) {
        this.text = text;
    }

    protected Style getStyle() {
        return style;
    }

    protected String getText() {
        return text;
    }

    public LabelFactory with(Style style) {
        this.style = style;
        return this;
    }

    public JLabel build() {
        JLabel label = new JLabel(getText());
        Font font = label.getFont();
        switch (getStyle()) {
            case TITLE:
                label.setFont(font.deriveFont(Font.BOLD, font.getSize() * 3));
                break;
            case SUB_TITLE:
                label.setFont(font.deriveFont(Font.BOLD, font.getSize() * 2));
                break;
        }
        return label;
    }
}

I found myself wanting to modify some of the text, so I used a "factory" to generate labels with different styles. Because we're relying on the default label font and size, we're actually getting some dynamic support, always nice.

public class RecipesListPane extends JPanel {
    public interface Observer {
        public void didSelectRecipeBook(RecipesListPane source, RecipeBook book);
    }

    private JList<RecipeBook> recipesList;

    public RecipesListPane(RecipeBook[] recipeBooks, Observer observer) {
        recipesList = new JList<>(recipeBooks);
        recipesList.setCellRenderer(new RecipeBookListCellRender());

        setLayout(new BorderLayout());
        add(new LabelFactory("Recipes").with(LabelFactory.Style.TITLE).build(), BorderLayout.NORTH);
        add(new JScrollPane(recipesList));

        recipesList.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                RecipeBook book = recipesList.getSelectedValue();
                if (book == null) {
                    return;
                }
                observer.didSelectRecipeBook(RecipesListPane.this, book);
                recipesList.setSelectedValue(null, false);
            }
        });
    }

    public class RecipeBookListCellRender extends DefaultListCellRenderer {
        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            if (value instanceof RecipeBook) {
                RecipeBook book = (RecipeBook) value;
                value = book.getName();
            }
            return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        }
    }
}

This displays the list of recipe books (ie it's a book case!). Please note, I've enclosed the renderer within the class, you might find it more useful to have this class separated into it's own context which would make it more re-usable. But also note, the renderer is been used to make choices about "how" the books are displayed to the user.

For example, you could add support for images at some point in the future and the renderer could then be updated to display them if and when you wanted to.

public class RecipeBookPane extends JPanel {
    public interface Observer {
        public void didSelectRecipe(RecipeBookPane source, Recipe recipe);

        public void didNavigateBack(RecipeBookPane source);
    }

    private JList<Recipe> recipesList;
    private JLabel recipeBookTitleLabel;

    public RecipeBookPane(Observer observer) {
        recipesList = new JList<>();
        recipesList.setCellRenderer(new RecipeListCellRender());

        setLayout(new BorderLayout());

        JButton backButton = new JButton("<");
        backButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                observer.didNavigateBack(RecipeBookPane.this);
            }
        });

        JPanel titlePane = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1;
        gbc.anchor = GridBagConstraints.LINE_START;
        gbc.gridwidth = GridBagConstraints.REMAINDER;

        recipeBookTitleLabel = new LabelFactory("Book title goes here")
                .with(LabelFactory.Style.TITLE)
                .build();

        titlePane.add(backButton, gbc);

        gbc.fill = GridBagConstraints.HORIZONTAL;
        titlePane.add(recipeBookTitleLabel, gbc);
        titlePane.add(new LabelFactory("Recipe Book").with(LabelFactory.Style.SUB_TITLE).build(), gbc);

        add(titlePane, BorderLayout.NORTH);
        add(new JScrollPane(recipesList));

        recipesList.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                Recipe recipe = recipesList.getSelectedValue();
                if (recipe == null) {
                    return;
                }
                observer.didSelectRecipe(RecipeBookPane.this, recipe);
                recipesList.setSelectedValue(null, false);
            }
        });
    }

    public void setRecipeBook(RecipeBook book) {
        // Because this is dynamic, this could have an adverse effect on the
        // packed size of the window, just beware
        recipeBookTitleLabel.setText(book.getName());
        DefaultListModel model = new DefaultListModel<Recipe>();
        for (Recipe recipe : book.getRecipes()) {
            model.addElement(recipe);
        }
        recipesList.setModel(model);
    }

    public class RecipeListCellRender extends DefaultListCellRenderer {
        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            if (value instanceof Recipe) {
                Recipe book = (Recipe) value;
                value = book.getName();
            }
            return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        }
    }
}

This simply displays all the recipes for a given book. Normally I'd use dependency Injection and pass the RecipeBook via the constructor, but that's not really how CardLayout works (you could make it work that way, but it becomes messy), so instead, the RecipeBook is passed via a setter.

public class RecipePane extends JPanel {
    public interface Observer {
        public void didNavigateBack(RecipePane source);
    }

    private JList<Recipe> inredientsList;
    private JLabel recipeTitleLabel;

    public RecipePane(Observer observer) {
        inredientsList = new JList<>();
        inredientsList.setCellRenderer(new IngredientListCellRender());

        setLayout(new BorderLayout());

        JButton backButton = new JButton("<");
        backButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                observer.didNavigateBack(RecipePane.this);
            }
        });
        JPanel titlePane = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1;
        gbc.anchor = GridBagConstraints.LINE_START;
        gbc.gridwidth = GridBagConstraints.REMAINDER;

        recipeTitleLabel = new LabelFactory("Recipe title goes here")
                .with(LabelFactory.Style.TITLE)
                .build();

        titlePane.add(backButton, gbc);
        gbc.fill = GridBagConstraints.HORIZONTAL;
        titlePane.add(recipeTitleLabel, gbc);
        titlePane.add(new LabelFactory("Recipe Book").with(LabelFactory.Style.SUB_TITLE).build(), gbc);

        add(titlePane, BorderLayout.NORTH);
        add(new JScrollPane(inredientsList));

        inredientsList.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
            }
        });
    }

    public void setRecipe(Recipe recipe) {
        // Because this is dynamic, this could have an adverse effect on the
        // packed size of the window, just beware
        recipeTitleLabel.setText(recipe.getName());
        DefaultListModel model = new DefaultListModel<Recipe>();
        for (Ingredient ingredient : recipe.getIngredients()) {
            model.addElement(ingredient);
        }
        inredientsList.setModel(model);
    }

    public class IngredientListCellRender extends DefaultListCellRenderer {
        private NumberFormat formatter;

        public IngredientListCellRender() {
            formatter = NumberFormat.getNumberInstance();
            formatter.setMinimumFractionDigits(0);
        }

        @Override
        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            if (value instanceof Ingredient) {
                // You could, very easily, decouple the formatting of the
                // ingredients and units to there own format implementations,
                // allowing a greater level of flexibility in how
                // certain elements are formatted.  You could then use
                // dependency injection to pass the formatter(s) into this
                // class, allowing for a greater level of customisation
                Ingredient ingredient = (Ingredient) value;
                Measurement measurement = ingredient.getMeasurement();
                StringBuilder sb = new StringBuilder(64);
                sb.append(formatter.format(measurement.getQuantity()))
                        .append(" ");
                switch (measurement.getUnit()) {
                    case CUP:
                        sb.append(" Cups ");
                        break;
                    case GRAMS:
                        sb.append(" Grams ");
                        break;
                    case MILLI_LETER:
                        sb.append(" mls ");
                        break;
                    case TEA_SPOON:
                        sb.append(" Tea spoons ");
                        break;
                }
                sb.append(ingredient.getName());

                value = sb.toString();
            }
            return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
        }
    }
}

And finally, the recipe itself, displaying the ingredients.

There's a repeating pattern here, can you see it? The "back" button implementation is been repeated!

While not serious, it is something that could come back to haunt us if we choice to change the navigation workflow, so, instead, lets try and seperate it.

public class NavigationPane extends JPanel {
    public interface Observer {
        public void didNavigateBack(NavigationPane source);
    }

    private JButton backButton;
    private Observer observer;

    public NavigationPane(JPanel content, Observer observer) {
        this.observer = observer;
        setLayout(new BorderLayout());

        JPanel navigationPane = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1;
        gbc.anchor = GridBagConstraints.LINE_START;
        navigationPane.add(getBackButton(), gbc);

        add(navigationPane, BorderLayout.NORTH);
        add(content);
    }

    public JButton getBackButton() {
        if (backButton != null) {
            return backButton;
        }
        backButton = new JButton("<");
        backButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                navigateBack();
            }
        });
        return backButton;
    }

    public Observer getObserver() {
        return observer;
    }

    protected void navigateBack() {
        Observer observer = getObserver();
        if (observer == null) {
            return;
        }
        observer.didNavigateBack(this);
    }
}

This is a pretty simple wrapper class which simply wraps the navigation UI around a pre-existing UI element.

We can start by changing the RecipePane to remove the navigation workflow...

public class RecipePane extends JPanel {
    //...
    public RecipePane() {
        inredientsList = new JList<>();
        inredientsList.setCellRenderer(new IngredientListCellRender());

        setLayout(new BorderLayout());
        JPanel titlePane = new JPanel(new GridBagLayout());
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.weightx = 1;
        gbc.anchor = GridBagConstraints.LINE_START;
        gbc.gridwidth = GridBagConstraints.REMAINDER;

        recipeTitleLabel = new LabelFactory("Recipe title goes here")
                .with(LabelFactory.Style.TITLE)
                .build();

        gbc.fill = GridBagConstraints.HORIZONTAL;
        titlePane.add(recipeTitleLabel, gbc);
        titlePane.add(new LabelFactory("Recipe Book").with(LabelFactory.Style.SUB_TITLE).build(), gbc);

        add(titlePane, BorderLayout.NORTH);
        add(new JScrollPane(inredientsList));

        inredientsList.addListSelectionListener(new ListSelectionListener() {
            @Override
            public void valueChanged(ListSelectionEvent e) {
                // Do you want to do something here?!
            }
        });
    }
    //...
}

And update the RecipeLibraryPane to support it...

public class RecipeLibraryPane extends JPanel {
    //...
    public RecipeLibraryPane(RecipeBook[] books) {
        //...
        recipePane = new RecipePane();
        //...
        add(new NavigationPane(recipePane, new NavigationPane.Observer() {
            @Override
            public void didNavigateBack(NavigationPane source) {
                show(View.RECIPE_BOOK_CONTENTS);
            }
        }), View.RECIPE);
        //...
    }
}

And now we're better supporting the Single Responsibility Principle (as the navigation UI is been managed by a seperate class independent from the other class, which really didn't care about navigation)

(nb: I've not changed RecipeBookPane to support this, I'll leave that to you)

And finally, how do we actually launch this UI? Maybe by place something like...

EventQueue.invokeLater(new Runnable() {
    @Override
    public void run() {
        RecipeBook[] books = new RecipeBook[]{
            new DefaultRecipeBook(
            "Grandmas",
            new Recipe[]{
                new RecipeBuilder("Banana Bread")
                .with(new DefaultIngredient(Ingredient.BANANAS, new DefaultMeasurement(6, Unit.EACH)))
                .with(new DefaultIngredient(Ingredient.UNSALTED_BUTTER, new DefaultMeasurement(30, Unit.GRAMS)))
                .with(new DefaultIngredient(Ingredient.GRANULATED_SUGAR, new DefaultMeasurement(0.75, Unit.CUP)))
                .with(new DefaultIngredient(Ingredient.ALL_PURPOSE_FLOUR, new DefaultMeasurement(1, Unit.CUP)))
                .with(new DefaultIngredient(Ingredient.BAKING_SODA, new DefaultMeasurement(1, Unit.TEA_SPOON)))
                .with(new DefaultIngredient(Ingredient.SALT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
                .with(new DefaultIngredient(Ingredient.VANILLA_EXTRACT, new DefaultMeasurement(1, Unit.TEA_SPOON)))
                .build()
            })
        };

        JFrame frame = new JFrame();
        frame.add(new RecipeLibraryPane(books));
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
});

into your main method.

You should also take the time to have a read of:

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
-2

To answer your immediate question, you have the aforementioned labels set as package-private, meaning that they do not have an access modifier in front of them. Because of this, all of the classes in the sweets package have access to the labels, because those labels are package-private.

So, you can do something as simple as this.

In your MainFrame class, you currently have this line.

bananaBreadIngredientsJPanel = new bananaBreadIngredients();

Well, change the constructor to take in an instance of MainFrame.

Meaning, you currently have this constructor in your bananaBreadIngredients class.

public bananaBreadIngredients()

Well, just change it to be like this instead.

public bananaBreadIngredients(MainFrame mainFrame)

Once you have done that, then your bananaBreadIngredients class now has access to every JLabel in MainFrame that is set to package-private. In fact, it has access to every Class Variable that is also set to package-private, amongst other things (that aren't relevant right now).

So, to access each desired JLabel from MainFrame, simply reference it using the passed in instance of MainFrame in your newly modified constructor, like this.

public bananaBreadIngredients(MainFrame mainFrame){

    JLabel oneOfTheLabels = mainFrame.granulatedSugar;
    //now you can do stuff with this variable.
    //repeat for the other variables

}

This should do it.


But I have to say, your current class structure and organization is going to get unwieldy very quickly (even more so after the answer I gave you).

I would strongly encourage you to work on improving your use of strategies like loose-coupling and Java enums. The strategy you have now will bite you in the butt, assuming that it isn't already doing that.

davidalayachew
  • 1,279
  • 1
  • 11
  • 22
  • 2
    No, don't do this, this is just putting fuel on the fire of bad ideas and direction. Fields should be `private` and accessible via getters and/or setters, as required. Exposing the `MainFrame`, which is already a bad place to start, to `bananaBreadIngredients` (shell we discuss the bad naming conventions) is giving `bananaBreadIngredients` way more control over `MainFrame` then it should have or be responsible for. The better approach would be to break the requirements down, focus on the data, build appropriate models and the make use the model-view-controller concept to drive the UI – MadProgrammer Jul 30 '23 at 06:28
  • @MadProgrammer You are preaching to the choir. I put that last section at the end to address your points specifically. – davidalayachew Jul 30 '23 at 06:48
  • @MadProgrammer But I'm not going to not answer their question either. The fact is, they have a question, I'll answer it with respect to the design decisions they made, but I'll also tell them that their design decisions are poor. – davidalayachew Jul 30 '23 at 06:49
  • 1
    Thanks for creating a self fulfilling prophesy which the rest of us well have to clean up. There is a difference between an answer which might solve the problem and a correct answer which teaches them to solve simular problems in the future. IMHO in this case, it would be better to tell them that they are off track and provide guidance to put them on track over directly "answering the question", because this is just going to make them ask more questions about there poor design choices which further digs them into a hole. It's the "give a fish or teach to fish" parable – MadProgrammer Jul 30 '23 at 10:35
  • @MadProgrammer I haven't been doing this as long as you have, but I've seen your alternative. I understand that you don't want to clean up other people's mess, so that is a fair criticism to make, but I would consider that a better alternative than turning people away from the practice. – davidalayachew Jul 30 '23 at 14:05
  • As a general fact of teaching, fixing one problem at a time is a better strategy than telling a student to completely uproot their strategy. As if that is a safe assumption to make about most students. – davidalayachew Jul 30 '23 at 14:08
  • So... here's my point - this question, not only has been asked many times before, so we should be closing it as a duplicate, could just as easily be "answered" by directing the OP to [Passing Information to a Method or a Constructor](https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html), so the question (and answer) are not adding a new value to SO or future problem seekers, if anything, it's actually pointing people in the wrong direction. – MadProgrammer Jul 30 '23 at 21:32
  • To my "teaching" point, the fact is, this isn't the first question the OP has made with this code and not the first time they've been told to "take a step back" and re-approach the problem. In fact, I would, as a professional developer, highly regard this ability, sometimes it's better to start a fresh then continue down a rabbit hole just because of the amount of time you've invested into your current solution. Also a teacher for many years, I would always prefer to pull a student back to the edge and reinforce some basic then watch them drown ;) – MadProgrammer Jul 30 '23 at 21:33
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254723/discussion-between-davidalayachew-and-madprogrammer). – davidalayachew Jul 30 '23 at 21:56