Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Swing

Construct data model, view, and control

4.67/5 (3 votes)
26 Mar 2014CPOL4 min read 12.1K   246  
Example of JNotif - A Java Notification App

Introduction

Based on a 'Conscience' project in my Software Engineering course, and a recent individual project 'TodayThought', the idea of this JNotif is to let the users save (important or favorite) notes, quotes, or messages. Then, the user can choose how and when those information remind him/her:

  • display those messages on the computer screen
  • can re-position and re-size this area
  • Size and location are recorded and resumed the next time the program starts
  • can set font family, font size, front and background color
  • schedule time to display
  • optional audio notifications

Plan

The data model created first, because it is the fundation and heavy-lifting of the whole program. Then the GUI view of the data, and the control that manipulates data and set the view.

1. Data Model

XML is used in this program. We will need the following:

popups.xml

popups.xml is to store all messages, quotes, or notes. Each of these item is a popup. Structure is as follow:

XML
<?xml version="1.0" encoding="utf-8"?>
<popups>
    <popup id="1">
        <text>Lorem ipsum, dolor sit amet, nullam wisi scelerisque velit.</text>
    
    <popup id="2">
        <text>Ac ante pellentesque, erat venenatis gravida mauris felis id id.</text>
    </popup>    
        ...

We will use three classes to work with this popups.xml: Popup, Popups, and PopupParser.

Popup.class to model individual popup is:

Java
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "popup")
public class Popup implements {

    private int id;
    private String text;

    ... Regular constructors and getters 

    @XmlAttribute(name = "id")
    public void setId(int id) {
        this.id = id;
    }

    @XmlElement(name = "text")
    public void setText(String text) {
        this.text = text;
    }
}

Popups class to model a list of popups:

Java
import java.util.ArrayList;
import java.util.Random;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "popups")
public class Popups {

    private ArrayList<Popup> popups;

    public ArrayList<Popup> getPopups() {
        return this.popups;
    }

    public int getSize() {
        return this.popups.size();
    }

    @XmlElement(name = "popup")
    public void setPopups(ArrayList<Popup> popups) {
        this.popups = popups;
    }

    public Popup getPopup(int index) {
        return this.popups.get(index);
    }

    public Popup getRandomPopup() {
        Random random = new Random();
        int randomNum = random.nextInt(this.getSize());
        return this.getPopup(randomNum);
    }

    public void addPopup(Popup popup) {
        //popup.setId(this.popups.get(this.popups.size() - 1).getId() + 1);
        this.popups.add(popup);
        this.reIndex();
    }

    // Remove and re-index popups
    public void removePopup(int index) {
        this.popups.remove(index);
        this.reIndex();
    }

    public void removeLastPopup() {
        this.removePopup(this.popups.size() - 1);
    }

    public void removeFirstPopup() {
        this.removePopup(0);
    }

    private void reIndex() {
        for (int i = 1; i <= this.popups.size(); i++) {
            this.popups.get(i - 1).setId(i);
        }
    }
}

And PopupParser is used to parse XML data from popups.xml AND write data back to that file.

Java
import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class PopupParser {

    private Popups popups;
    private int popupIndex;
    File file;

    public PopupParser(String file) throws JAXBException {
        this.file = new File(file);
        this.readPopups();
    }

    // Read list of popups from popups.xml
    private void readPopups() throws JAXBException {
        JAXBContext jAXBContext = JAXBContext.newInstance(Popups.class);

        Unmarshaller unmarshaller = jAXBContext.createUnmarshaller();
        this.popups = (Popups) unmarshaller.unmarshal(this.file);
    }

    // Write popups to popups.xml
    public void writePopupsToXMLFile(Popups popups) throws JAXBException {
        JAXBContext jAXBContext = JAXBContext.newInstance(Popups.class);
        Marshaller marshaller = jAXBContext.createMarshaller();

        // Output pretty format
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(popups, this.file);
    }

    public int getNumPopups() {
        return this.popups.getSize();
    }

    public Popup getPopup(int index) {
        return this.popups.getPopup(index);
    }

    public Popup getNextPopup() {
        if (this.popupIndex >= this.popups.getSize()) {
            this.popupIndex = 0;
        }
        return this.popups.getPopup(popupIndex++);
    }

    public Popup getPrevPopup() {
        if (this.popupIndex <= 0) {
            this.popupIndex = this.popups.getSize() - 1;
        }
        return this.popups.getPopup(popupIndex--);
    }

    public Popup getRandomPopup() {
        return this.popups.getRandomPopup();
    }

    public Popups getPopups() {
        return this.popups;
    }
}

themes.xml

This XML is to store themes' settings. The structure is as below:

XML
<?xml version="1.0" encoding="utf-8"?>
<themes>
    <theme id="1">
        <audio_alert>Windows Information Bar.wav</audio_alert>
        <msg_format>
            <color_background>#333333</color_background>
            <font_family>Tahoma</font_family>
            <font_size>14</font_size>
            <font_size_title_bar>8</font_size_title_bar>
            <position>bottom, right</position>
            <color_text>#FFFFFF</color_text>
        </msg_format>
        <name>Default</name>
    
    
    ... More themes

We notice that, because of the 'nested' structure of this XML, to model a theme we use an additional class that represents a message format. So, besides a name and an audio alert, the Theme class uses MsgFormat as it data member. Definition of MsgFormat class is:

Java
// Import packages

public class MsgFormat {

    private String fontFamily;
    private int msgFontSize;
    private int msgFontTitleSize;
    private Color bgColor;
    private Color textColor;
    private String msgPosition;

    public MsgFormat(String fontFamily, int msgFontSize,
            Color gbColor, Color textColor) {
        this(fontFamily, msgFontSize, 8, gbColor, textColor, "bottom, right");
    }

    public MsgFormat(String fontFamily, int msgFontSize, int msgFontTitleSize,
            Color gbColor, Color textColor, String msgPosition) {
        this.fontFamily = fontFamily;
        this.msgFontSize = msgFontSize;
        this.msgFontTitleSize = msgFontTitleSize;
        this.bgColor = gbColor;
        this.textColor = textColor;
        this.msgPosition = msgPosition;
    }

    @XmlElement(name = "font_size_title_bar")
    public void setMsgFontTitleSize(int msgFontTitleSize) {
        this.msgFontTitleSize = msgFontTitleSize;
    }

    @XmlJavaTypeAdapter(ColorAdapter.class)
    public Color getBgColor() {
        return bgColor;
    }

    ... Getters and Setters here
}

The ColorAdapter class is used above to convert between String and Color as needed:

Java
import java.awt.Color;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class ColorAdapter extends XmlAdapter<String, Color> {

    @Override
    public Color unmarshal(String s) {
        return Color.decode(s);
    }

    @Override
    public String marshal(Color color) {
        String hex = Integer.toHexString(color.getRGB()).toUpperCase();
        hex = hex.substring(2, hex.length());

        return '#' + hex;
    }
}

Now back to the Theme class:

Java
... Import packages here

@XmlRootElement(name = "theme")
public class Theme implements Cloneable {

    private int id;
    private String name;
    private String audioAlert;
    private MsgFormat msgFormat;

    public Theme(String name, String audio, MsgFormat format) {
        this.audioAlert = audio;
        this.name = name;
        this.msgFormat = format;
    }

    public int getId() {
        return id;
    }

    ... Similiar Getters and Setters here

    public MsgFormat getFormat() {
        return msgFormat;
    }

    @XmlElement(name = "msg_format")
    public void setFormat(MsgFormat msgFormat) {
        this.msgFormat = msgFormat;
    }
}

Themes and ThemeParser are the similiar with Popups and PopupParser above.

config.xml

This XML is to save configuration settings:

XML
<?xml version="1.0" encoding="utf-8"?>
<config>
    <audio_volume>50.0
    <display_time>3</display_time>
    <frequency>3</frequency>
    <em>true</em>

Because there is only one config, we don't need Configs.class. Two classes are Config and ConfigParser, implemented the same way as above.

2. View

A dialog is used to display individual popup:
Image 1

Besides from GUI (which is achieved with Netbeans IDE) and threading (to improve the responsiveness), the are some features implemented:

  • This dialog is movable and resizable
  • The location, and size is serialized into a property file. So the program remembers these settings for later uses.

These two methods are used to store and retrieve the location and size on the screen of this popup dialog:

Java
// Store and Restore last position of dialog/frame
    public void storeOptions(JDialog dialog) throws IOException {
        File file = new File(this.locPropertyFile);
        Properties properties = new Properties();

        Rectangle r = dialog.getBounds();
        int x = (int) r.getX();
        int y = (int) r.getY();
        int w = (int) r.getWidth();
        int h = (int) r.getHeight();

        properties.setProperty("x", "" + x);
        properties.setProperty("y", "" + y);
        properties.setProperty("w", "" + w);
        properties.setProperty("h", "" + h);

        BufferedWriter br = new BufferedWriter(new FileWriter(file));
        properties.store(br, "Properties of the user frame");
    }

    public void restoreOptions(JDialog dialog) throws IOException {
        File file = new File(this.locPropertyFile);
        Properties p = new Properties();
        BufferedReader br = new BufferedReader(new FileReader(file));
        p.load(br);

        int x = Integer.parseInt(p.getProperty("x"));
        int y = Integer.parseInt(p.getProperty("y"));
        int w = Integer.parseInt(p.getProperty("w"));
        int h = Integer.parseInt(p.getProperty("h"));

        Rectangle r = new Rectangle(x, y, w, h);
        dialog.setBounds(r);
    }

Besides this, there are other GUI, such as the popups manager, themes manager, and settings Frame/Panel to let the user manage (delete, update, save popups, themes, and settings). Not sure that these GUIs belong to the 'View' or 'Control' in MVC practice. Anyway, doesn't matter much.

Settings panel:
Image 2

Themes manager frame:
Image 3

Popups manager frame:
Image 4

3. Control

The entry point of the program, class ControlNotif, and the program's System Tray are designed to be the 'control' role in this program. ControlNotif first creates all necessary things (data, components, objects): system tray, frame/panel/dialog, etc.
Program's system tray:
Image 5

Java
// System Tray
        sysTray = new SystemTrayNotif("images/quotes-icon.png", this.popupParser,
                this.themeParser, this.configParser, this.dialogNotif, this);
        Thread threadSysTray = new Thread(sysTray);
        threadSysTray.start();

Java
// Themes manager frame
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                frameThemeManager = new FrameThemesManager(themeParser);
            }
        });

This loop is to display popup according to the user settings:

Java
// Notification
        while (true) {
            if (this.isPaused == false) {

                // Play audio
                if (this.isAudioEnable) {
                    AudioNotif audioNotif = new AudioNotif(this.theme.getAudioAlert(),
                            this.config.getAudioVolume());
                    Thread audioThread = new Thread(audioNotif);
                    audioThread.start();
                }

                // Show DialogNotif
                new DialogNotif(this.popupParser,
                        this.theme, this.config).showPopup();

                Thread.sleep(this.config.getFreq() * 1000);
            } else {
                Thread.sleep(this.config.getFreq() * 1000);
            }
        }

Optional audio notification is implemented by the AudioNotif class:

Java
... Import needed packages here

public class AudioNotif implements Runnable {

    String audioFolder = "audio";
    String audioFile = "";
    double volume = 50.0;

    public AudioNotif(String fileName, double vol) {
        this.audioFile = this.audioFolder + "/" + fileName;
        this.volume = vol;
    }

    @Override
    public void run() {
        try {
            AudioInputStream audioInputStream
                    = AudioSystem.getAudioInputStream(new File(this.audioFile));

            AudioFormat baseFormat = audioInputStream.getFormat();
            AudioFormat decodedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                    baseFormat.getSampleRate(), 16, baseFormat.getChannels(),
                    baseFormat.getChannels() * 2, baseFormat.getSampleRate(), false);
            try (AudioInputStream decodedAs = AudioSystem.getAudioInputStream(decodedFormat, audioInputStream); Clip clip = AudioSystem.getClip()) {
                clip.open(decodedAs);

                FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);

                this.volume *= 0.01; // Min 0 to Max 1.0
                float dB = (float) (Math.log(this.volume) / Math.log(10.0) * 20.0);
                gainControl.setValue(dB);

                clip.start();

                do {
                    Thread.sleep(100); // To have time to play
                } while (clip.isRunning());

                clip.stop();
            }

        } catch (UnsupportedAudioFileException | IOException | LineUnavailableException | InterruptedException ex) {
            Logger.getLogger(AudioNotif.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Summary and future development

I think this program is useful and fun, for busy users who need to be reminded of things, or for any users who just want to store their favorite popups (notes, quotes, or messages) and display them on their desktop, at the desired requency, time interval, and custom settings such as location, look and feel.

That's why I plan to further develop this program. Because the limitation of XML, embedded Java DB (Derby) will be employed to store a relational database of user popups and settings.
Next might be more customizable themes, themes package, Web integration, and social features.
The source-code for this article is not available at this time because it's being modified and tested with Derby.

Updates will be available on the JNotif website.

Credits

This program was inspired by a project in my Software Engineering class, all the XML Parsing was the work of Kevin Schuck, one of my team member.
While developing this program, many code samples, tutorials, and Q&A, especially from StackOverlow and Oracle Tutorials were helpful and used in this program.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)