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 aScene
, before we proceed to take asnapshot
of theCouponCard
. The main challenge came when trying to save theWritableImage
returned bysnapshot
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 thehelp
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:
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
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
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:
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 theedit
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
Examples:
-
expand 1
Opens the first coupon in the coupon list in a new 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.
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.
-
The format of dates entered via the
r/
field is in the D-M-YYYY format. (Coupon Stash date format)
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:
-
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
:
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:
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:
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 intoHistoryManager
. -
Model#undo()
— Restores the previous coupon stash state fromHistoryManager
. -
Model#redo()
— Restores a previously undone coupon stash state fromHistoryManager
.
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.
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.
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.
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…
.
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:
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.
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.
currStateIndex
is purged.The following activity diagram summarizes what happens when a user executes a new command text:
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
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
.
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.
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.
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.
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
.
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.
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
.
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 theSortCommand
class and this methods in theModel
interface andSortedList
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 theObservableList
of coupons that are stored inModel
according to the order decided by the passed incomparator
. -
SortedList#setComparator(Comparator<Coupon> comparator)
- Sets the comparator that determines the order of the coupons inside the sorted list.
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.
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.