PROJECT: Coupon Stash

Ui

Overview

Coupon Stash is a desktop coupons manager that allows users to keep track of their coupons. The user interacts with it using a CLI, and it has a beautiful GUI created with JavaFX. It is written in Java, and has about 15kLoC.

Summary of contributions

  • Major enhancement: added the ability to undo and redo operations (Pull request #91, #135, #284)

    • What it does: allows the user to undo the most recent operation on the coupon stash. Undoing can itself be undone via the redo operation.

    • Justification: This feature is essential in allowing users who’ve made mistakes in their operations on the coupon stash to revert their changes. Addtionally, users will also want to be able to revert to their reverted changes, and this led to the creation of the undo and redo commands.

    • Highlights: This enhancement required the deep copy of all a coupon’s fields so as to prevent unpredictable side effects down the line. Thus, there was constant collaboration and communication with the group members regarding how to best tie in this feature with the rest of the features. Some new commands added by other group members also require updates to be pushed to the undo and redo feature, thus this was a very dynamic feature that needed frequent updates.

  • Major enhancement: added the ability to share coupons as images (Pull request #176, #384)

    • What it does: allows the user to share their coupons as an image file.

    • Justification: In the 21st century where everyone seems to post every thing on social media, the ability for users to share their coupons as a format ripe for social media consumption is of upmost importance. This feature allows users to share their coupons in a portable format for sharing over various platforms.

    • Highlights: This enhancement required the rendering of a CouponCard in a Scene, before we proceed to take a snapshot of the CouponCard. The main challenge came when trying to save the WritableImage returned by snapshot as a .png file. To perform that task, an external module (javax.imageio.ImageIO) had to be requested to be used. Then, the image needed a save dialog to be saved. Due to file operations having potential for many errors, exceptions has to be caught and thrown and multiple error messages were also created to best reflect an error.

  • Major enhancement: added the ability to open an offline copy of the user guide (Pull request #201, #284, #385)

    • What it does: allows the user to open an offline copy of the user guide in their web browser with the help command.

    • Justification: While the help tab exists, the help tab only shows information about the commands. Much more of the program’s functionality is available in the user guide, and with the program being an offline application, it is thus very useful for the user to be able to read the user guide offline. This will empower users to fully exploit the potential of our program.

    • Highlights: The user guide is stored as a single .html file with all styles and images embed into that single ~6 MB file. The file has to be packaged into the release jar file, before it gets extracted from the jar file on the first time the help command is executed. It was a challenge to reliably get the path of the jar file. (We need the path to extract the .html from the jar) After figuring out how to get the jar file path during runtime, we had to extract the .html file from within the jar file and then write it to the directory the jar file resides. Initially, writing to a file was very slow and it would even make the program crash sometimes! Then, I remembered what I learnt from CS2106 and realised that a buffered writing operation will reduce system calls and thus be faster. And lo and behold, after the implementation of a write buffer, writing speeds got a lot faster. (now it only takes one second)

  • Minor enhancement: added a history command that allows the user to navigate to previous commands using up/down keys. (Pull request #135, #140)

  • Minor enhancement: added a sort command that allows the user to sort coupons by the names, expiry dates, and remind dates in ascending order. (Pull request #140, #284)

  • Minor enhancement: worked on the expand command. (Pull request #375)

  • Minor enhancement: wrote the logic for the Help Pane. (Pull request #215)

  • Minor enhancement: improved and cleaned up the remind command. (Pull request #329)

  • Code contributed: [All contributed code]

  • Other contributions:

    • Project management:

      • Managed releases v1.3 - v1.5rc (3 releases) on GitHub

    • Enhancements to existing features:

      • Updated the help menu #215

      • Updated the help command to open an offline copy of the user guide #284

    • Community:

      • Contributed to forum discussions (example: 1)

Contributions to the User Guide

Here are some sections of the User Guide authored by me.

Sorting coupons: sort

Sorts the coupons in Coupon Stash in ascending order. It is possible to sort by coupon name, expiry date, or remind date.

When you edit or add a coupon, the sorting order will revert to the original order. Otherwise, the sorting order will persist throughout the runtime of the program.
The command will sort any coupons currently on screen, including archived coupons, if they are present on screen. Archived coupons will always appear below active coupons.

Format: sort PREFIX

  • The PREFIX can be either n/, e/ or r/.

  • Using n/ would mean sorting by name, e/ would mean sorting by expiry date, and r/ would mean sorting by reminder date.

Examples:

  • sort n/
    Sorts the Coupon Stash by name in ascending order.

  • sort e/
    Sorts the Coupon Stash by expiry date from earliest to latest.

  • sort r/
    Sorts the Coupon Stash by reminder date from earliest to latest.

Undoing the previous command: undo

Undo the most recent operation on the Coupon Stash. Only operations that change the coupons in the Coupon Stash can be undone.

Format: undo

Undo only works on the following commands: (i.e. commands that change the coupons in the Coupon Stash)

Changes to user preferences, such as the setting of the currency symbol, cannot be undone.
You can only undo commands which you have input since you opened Coupon Stash. This means that after you close the application, your undo history will be lost!

Examples:

  • edit 1 r/ 25-12-2020
    undo
    Undo the edit command. Remind date of first coupon in the coupon list reverts back to its previous value.

  • delete 1
    undo
    Revert the delete that was performed. Deleted coupon is restored.

Redoing the undone command: redo

Redo the previously undone operation. This is akin to undoing an undo.

Format: redo

Examples:

  • edit 1 r/ 25-12-2020
    undo
    redo
    Un-undo the edit command. Remind date of first coupon that was in the resulting coupon list is edited.

  • delete 1
    undo
    redo

Un-undo the delete command. First coupon that was in the resulting coupon list is deleted.

Expanding a coupon: expand

Displays a coupon in its own window, giving you a complete expanded view of it.

Format: expand INDEX

  • Expands the coupon at the specified INDEX.

Examples:

  • expand 1
    Opens the first coupon in the coupon list in a new window.

expand example
Figure 1. Example of an expanded coupon window.

Reminding of coupon expiry

By default, all added coupons have a remind date that is 3 days before its stated expiry date. This default value can be changed during the process of adding a new coupon or by editing an existing coupon. The remind dates for all coupons are checked on program launch. All coupons that have a remind date which falls on the date of program launch will be listed in a popup window on launch. The popup window can be closed by pressing Ctrl + q.

ug reminder window
Figure 2. Popup window showing all coupons that have their remind dates set on the date of program launch.
Remind dates are only checked during program launch. Thus, if a day passes while the program is open, even if there are coupons expiring on the new day, no new reminder window will open.

As of now, there is no functionality for the disabling of reminders for coupons. This feature will be made available in version 2.0.

Examples:

  • add n/McDonald’s McGriddles p/ILOVEMCGRIDDLES e/31-12-2020 s/$2 sd/1-4-2020 l/2 t/value t/savoury

    Add a new Coupon without specifying a remind date. Note that the default remind date is 3 days before the state expiry date:

ug mcdonalds mcgriddles
Figure 3. New coupon with a default remind date.
  • add n/Grabfood s/40% e/30-4-2020 p/GRAB40 t/delivery r/10-4-2020

    Add a new Coupon while specifying a remind date of 10-04-2020:

ug Grabfood reminder
Figure 4. New coupon with remind date set to 10-04-2020.
  • edit 1 r/10-4-2020

    Edit the coupon at index 1 and change its remind date to 10-4-2020.

Keyboard Shortcuts

With Coupon Stash being optimized for efficient command line usage, how can we not include some nifty keyboard shortcuts to further streamline your workflow! This section introduces some of the keyboard shortcuts available for use in this program.

Restore previously entered command texts: and

Pressing the keyboard button while the focus is on the command box allows you to restore the text of the previous executed command. Pressing the keyboard button allows you to revert to a more recently entered command.

Demonstration:

Up down arrow
Figure 5. Animation of / button presses.

Close expanded coupon or reminder windows: Ctrl+q

After expanding a coupon, you can close the expanded coupon window by keeping the focus on the coupon window and pressing the keyboard buttons Ctrl and q simultaneously. This shortcut also works for closing reminder windows.

Demonstration: during the process of adding .Animation of closing window with Ctrl+q. image::gifs/ctrl q.gif[]

Cycle through tabs: Ctrl+Tab

You can press the keyboard buttons Ctrl and Tab simultaneously to switch tabs. Do note that the focus has to be on the main panel before the key presses would work.

Demonstration:

ctrl tab
Figure 6. Animation of switching tabs with Ctrl+Tab.

Contributions to the Developer Guide

These are some sections I contributed to the Developer Guide. Highlights include frequent use of Unified Modelling Language (UML) diagrams to illustrate associations between program components.

Undo/Redo feature

The undo/redo mechanism is facilitated by with an undo/redo history, stored internally as an couponStashStateList with a commandTextHistory and currStateIndex. All these components are encapsulated in the HistoryManager class. The following methods in the Model interface facilitates this feature:

  • Model#commitCouponStash(String commandText) — Saves the current coupon stash state and the command text that triggered the change in state into HistoryManager.

  • Model#undo() — Restores the previous coupon stash state from HistoryManager.

  • Model#redo() — Restores a previously undone coupon stash state from HistoryManager.

Current Implementation

Given below is an example usage scenario and how the undo/redo mechanism behaves at each step.

Step 1. The user launches the application for the first time. The CouponStash will be initialized with the initial coupon stash state, and the currStateIndex pointing to that single coupon stash state.

UndoRedoState0
Figure 7. CouponStash will be initialized with the initial coupon stash state.

Step 2. The user executes delete 5 command to delete the 5th coupon in the coupon stash. The delete command calls Model#commitCouponStash(String commandText), causing the modified state of the coupon stash after the delete 5 command executes to be saved in the couponStashStateList, and the delete 5 command text to be stored in the commandTextHistory. currStateIndex is shifted to the newly inserted coupon stash state.

UndoRedoState1
Figure 8. currStateIndex is shifted to the newly inserted coupon stash state.

Step 3. The user executes add n/OMO STORE …​ to add a new coupon. The add command also calls Model#commitCouponStash(String commandText), causing another modified coupon stash state and command text to be saved into the couponStashStateList and commandTextHistory respectively.

UndoRedoState2
Figure 9. Modified coupon stash state and command text are saved into the couponStashStateList and commandTextHistory respectively.
If a command fails its execution, it will not call Model#commitCouponStash(String commandText), so the coupon stash state and command text will not be saved.

Step 4. The user now decides that adding the coupon was a mistake, and decides to undo that action by executing the undo command. The undo command will call Model#undoCouponStash(), which will shift the currStateIndex once to the left, pointing it to the previous coupon stash state, and restores the coupon stash to that state. Plus, the command text is returned, thus allowing for the display of the command that was undone. In this case, the command undone is add n/OMO STORE…​.

UndoRedoState3
Figure 10. currStateIndex shifted once to the left.
If the currStateIndex is at index 0, pointing to the initial coupon stash state, then there are no previous coupon stash states to restore. The undo command uses Model#canUndoCouponStash() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo.

The following sequence diagram shows how the undo operation works:

UndoSequenceDiagram
Figure 11. Undo operation sequence diagram.

The redo command does the opposite — it calls Model#redoCouponStash(), which shifts the currStateIndex once to the right, pointing to the previously undone state and command text, and restores the coupon stash to that state. Finally, it returns the redone command text.

If the currStateIndex is at index couponStashStateList.size() - 1, pointing to the latest coupon stash state, then there are no undone coupon stash states to restore. The redo command uses Model#canRedoCouponStash() to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo.

Step 5. The user then decides to execute the command list. Commands that do not modify the coupon stash, such as list, will not call Model#commitCouponStash(). Thus, the couponStashStateList remains unchanged.

UndoRedoState4
Figure 12. couponStashStateList remains unchanged.

Step 6. The user executes clear, which calls Model#commitCouponStash(). Since the currStateIndex is not pointing at the end of the couponStashStateList, all coupon stash states and command text history after the currStateIndex will be purged. We designed it this way because it no longer makes sense to redo the add n/OMO STORE …​ command. This is the behavior that most modern desktop applications follow.

UndoRedoState5
Figure 13. Command text history after the currStateIndex is purged.

The following activity diagram summarizes what happens when a user executes a new command text:

CommitActivityDiagram
Figure 14. Activity diagram representation of how command texts are saved when they are executed.

Design Considerations

Aspect: How undo & redo executes
  • Alternative 1 (current choice): Saves the entire coupon stash.

    • Pros: Easy to implement.

    • Cons: May have performance issues in terms of memory usage. Plus, have to perform deep copy of coupons when saving the coupon stash so as to prevent unwanted mutations.

  • Alternative 2: Individual command knows how to undo/redo by itself.

    • Pros: Will use less memory (e.g. for delete, just save the coupon being deleted).

    • Cons: We must ensure that the implementation of each individual command is correct.

Alternative 1 was chosen due to its relative simplicity and extensibility. Little to no modification needs to be made to each command that can be undone, thus reducing chances of new bugs surfacing. Additionally, the ability to undo operations such as clear will require alternative 2 to copy the entire coupon stash too, so both alternatives will have the same memory footprint in such a context. Finally, the real world performance impact of copying all coupons vs copying one is not very huge. Thus, the more extensible and simpler alternative 1 was chosen.

Up/down arrow retrieve command history

Current Implementation

The retrieving of command history via the up and down arrow keys is facilitated by the CommandTextHistory class. The command history is stored internally as a LinkedList used as a stack with a currIndex tracking the next command in the history to return. The following methods and attributes in the CommandTextHistory class facilitates this feature:

  • CommandTextHistory#add(String commandText)

  • CommandTextHistory#getDown()

  • CommandTextHistory#getUp()

  • CommandTextHistory#commandTextHistory

  • CommandTextHistory#currIndex

UpDownClassDiagram
Figure 15. Class diagram representation of the command history retrieving function.

Given below is an example usage scenario and how the up/down button presses behaves at each step.

Step 1. The user launches the application for the first time. The CommandTextHistory is initialized with a stack containing only an empty string (""), and the currIndex is set to 0.

UpDownHistory0
Figure 16. Stack containing only an empty string

Step 2. The user executes delete 1. CommandBox#handleCommandEntered() will call CommandTextHistory#add(String commandText) to save the entered command into the stack contained in CommandTextHistory. The top of the stack (i.e. the empty string) is popped off first, before the entered command is pushed onto the stack. Then, the empty string is pushed onto the stack again, thus ensuring that the empty string stays at the top of the stack. Note that currIndex is not affected.

UpDownHistory1
Figure 17. Stack after executing delete 1

Step 3. The user executes delete 2. CommandBox#handleCommandEntered() will also save the entered command into the stack contained in CommandTextHistory. As in the previous step, the new command is pushed to the top of the stack, just below the empty string.

UpDownHistory2
Figure 18. Stack after executing delete 2.

Step 3. Now, the user decides to delete the second coupon again. We press the arrow key up once, and CommandBox#commandTextField has a listener that calls CommandTextHistory#getUp(). The currIndex is incremented, and then the command text pointed to by currIndex is returned and displayed in the program command box.

UpDownHistory3
Figure 19. After pressing the "up" arrow key.

Step 4. The user then executes the retrieved command (delete 2). As in the previous steps, this newly executed command is pushed to the top of the stack just below the empty string. However, in such a case when the currIndex is not 0 and does not point to the top of the stack, it is reset to 0.

UpDownHistory4
Figure 20. Stack after executing delete 2 again.
If the currStateIndex is pointing to the top of the stack, then there are no previous commands to retrieve. Thus, the up button will simply return the empty string. No changes to the stack and currIndex will be effected.

The down arrow key does the opposite, it will lead to the calling of CommandTextHistory#getDown(), which shifts the currIndex one item higher (i.e. decrement the currIndex by 1), before returning the command text pointed by the updated currIndex.

If the currIndex is at index commandTextHistory.size() - 1, pointing to the bottom of the stack, there is no next command to retrieve when pressing the down key. Thus, the down button will simple return the command text currently being pointed to by the currIndex. No changes to the stack and currIndex will be affected.

Below is a sequence diagram describing the events that happen when a user presses a key.

UpDownSequenceDiagram
Figure 21. Sequence diagram representing retrieval of command text history with the up and down arrow keys.

Below is a sequence diagram describing the events that happen when a executes a command text, thus triggering the saving of a command text into CommandTextHistory.

UpDownSequenceDiagramAdd
Figure 22. Sequence diagram representing the saving of a command text.

Design Considerations

Aspect: Data structure to support the key actions
  • Alternative 1 (current choice): Use LinkedList as a stack to store the command text history.

    • Pros: LinkedList is a better data structure that allows for more efficient operations supported by stacks.

  • Alternative 2: Use ArrayList as a stack to store the command text history.

    • Pros: ArrayList is more recognizable to people who are relatively new to Java, thus reducing confusion.

    • Cons: Stack operations are less efficient on ArrayList s. === Coupon Sorting ==== Current implementation The sorting of coupons in the coupon stash is facilitated by the following static variables in the SortCommand class and this methods in the Model interface and SortedList class.

  • SortCommand#NAME_COMPARATOR - Comparator that sorts coupons by name in ascending order.

  • SortCommand#EXPIRY_COMPARATOR - Comparator that sorts coupons by expiry date in ascending order.

  • SortCommand#REMINDER_COMPARATOR - Comparator that sorts coupons by remind date in ascending order.

  • Model#sortCoupons(Comparator<Coupon> comparator) - Sorts the ObservableList of coupons that are stored in Model according to the order decided by the passed in comparator.

  • SortedList#setComparator(Comparator<Coupon> comparator) - Sets the comparator that determines the order of the coupons inside the sorted list.

SortingClassDiagram
Figure 23. Overview of the class diagram representation of the coupon sorting implementation.

When a sort command is executed, the field to sort by is indicated by the inputted prefix. The sequence diagram below describes what happens when a sort command is run.

SortCommandSeqDiagram
Figure 24. Sequence diagram describing the process of sorting coupons.

Depending on the prefix to sort by, ModelManager#sortCoupons() will be called with the relevant comparator as its argument. The ModelManager#sortCoupons() method subsequently calls the SortedList#setComparator() method (not shown in the above diagram), which leads to a change of the comparator of the SortedList stored in ModelManager , thus triggering a sort of the SortedList.

Design Considerations

Aspect: Persistent or non - persistent sort?
  • Alternative 1 (current choice): Make sorting non - persistent.

    • Pros: Sorting is faster as no write to disk is needed to make the new order persistent. Additionally, with the coupons being sorted by the time they are added to the coupon stash by default, there is no way to restore this order without storing the time a coupon was added to the stash. Thus, the non - persistent approach shines here as restoring the original order of the coupon stash is as trivial as reopening the program.

    • Cons: If a user prefers a particular default sorting order for their coupons, they have to retype the sort command each time the program is launched or each time a coupon is added or edited.

  • Alternative 2: Make sorting persistent.

    • Pros: Gives users more freedom over the default order of their coupons.

    • Cons: Can be unnecessarily complicated to implement a hidden field stating a coupon’s addition time just so users can revert to the default order. Additionally, it can be confusing to users when there are so many different ways to sort.

In our usage during testing, we have never had the urge to have a default sorting order when the program is launched. Plus, we feel that the simplicity of excluding a sort by default order function will be well favored by users, and thus we chose alternative 1.