diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index b6bbfbaf446..4f55ef48484 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -44,14 +44,15 @@ The following class plays an important role at the architecture level: * `LogsCenter` : Used by many classes to write log messages to the App's log file. -The rest of the App consists of four components. +The rest of the App consists of five components. * <>: The UI of the App. * <>: The command executor. +* <>: The UI action handler. * <>: Holds the data of the App in-memory. * <>: Reads data from, and writes data to, the hard disk. -Each of the four components +Each of the five components * Defines its _API_ in an `interface` with the same name as the Component. * Exposes its functionality using a `{Component Name}Manager` class. @@ -69,6 +70,11 @@ The _Sequence Diagram_ below shows how the components interact with each other f .Component interactions for `delete 1` command image::ArchitectureSequenceDiagram.png[] +The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user switches tabs in the GUI. + +.Component interactions for Switching Tab UI Action +image::ArchitectureSequenceDiagram1.png[] + The sections below give more details of each component. //@@author jiayushe @@ -115,6 +121,30 @@ image::DeleteSequenceDiagram.png[] NOTE: The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. //@@author +//@@author tiuweehan +[[Design-UiLogic]] +=== UiLogic Component + +.Structure of the UiLogic Component +image::UiLogicClassDiagram.png[width="65%"] + +*API* : +link:{repoURL}/src/main/java/seedu/algobase/ui/action/UiLogic.java[`UiLogic.java`] + +. Performing an action (e.g. switching tabs) triggers the creation of a `UiActionDetails` object. +. `UiLogic` uses the `AlgoBaseUiActionParser` class to parse the `UiActionDetails` object. +. This results in a `UiAction` object which is executed by the `UiLogicManager`. +. The command execution can affect the `Model` (e.g. deleting a problem). +. The result of the command execution is encapsulated as a `UiActionResult` object which is passed back to the `Ui`. +. In addition, the `UiActionResult` object can also instruct the `Ui` to perform certain actions, such as displaying the results as feedback to the user. + +.Interactions Inside the UiLogic Component for a `UiActionDetails` with a `UiActionType` of `editPlanUiAction`. This `UiActionDetails` also contains the ID of the problem to be deleted, in this case `11b`. +image::EditSequenceDiagram.png[] + +NOTE: The lifeline for `EditProblemUiActionParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +//@@author + //@@author le0tan [[Design-Model]] @@ -154,8 +184,9 @@ image::design/model/ProblemSearchRulePackageDiagram.png[width="60%"] .Structure of the PlanSearchRule Package image::design/model/PlanSearchRulePackageDiagram.png[width='60%'] +.Structure of the GUI Package +image::design/model/GuiPackageDiagram.png[width='60%'] // end::model[] - //@@author //@@author jiayushe @@ -506,42 +537,142 @@ Since AlgoBase is forked from AddressBook 3, it also inherits AB3's design choic //@@author //@@author tiuweehan -// tag::switchTab[] - +// tag::gui === GUI Enhancements -An intuitive GUI facilitates the overall user friendliness of the application. The user should be able to navigate around the application easily to facilitate a smooth experience using AlgoBase. Additionally, multitasking is important as a user may be tackling multiple algorithmic questions at a single time. One of the main enhancements that we introduced to the GUI is tabbing, which fulfils these 2 requirements. +An intuitive GUI facilitates the overall user friendliness of the application. +The user should be able to navigate around the application easily to facilitate a smooth experience using AlgoBase. +While the command line is fast for typing short commands, it us not ideal if the user is editing large amounts of text (e.g. when the user is adding description for a new problem). +In this case, having a GUI will be more beneficial to the user and facilitates a smoother user experience. -==== Current Implementation +Additionally, multitasking is important as a user may be tackling multiple algorithmic questions at a single time. +This, we introduced tabbing, which facilitates multitasking in AlgoBase, which is an important requirement for competitive programmers. + +==== Editing Problems from GUI + +===== Current Implementation + +When the user makes a change in the GUI, the change is propagated from `Ui` to `UiLogic` to `Model` and to `Storage`, as represented in the diagram below. + +.An example of a high level representation of GUI actions. +image::ArchitectureSequenceDiagram1.png[] + +The following classes facilitate the handling of GUI actions: + +* `UiActionType` - An enum of the types of UI actions that exist in AlgoBase. +* `UiActionDetails` - An object containing details of a UI action. +* `UiAction` - Interface with instructions for executing a UI action. +* `UiLogicManager` - Manages the overall UI Logic. +* `AlgoBaseUiActionParser` - Parses a `UiActionDetails` object into an implementation of `UiAction`. +* `UiActionResult` - The result of executing the UI action. + +When the user performs any action in the GUI (e.g. Editing a problem), a `UiActionDetails` object is created with a given `UiActionType`, an enum type. +This object contains information about the action which will later be used to make changes to the application. + +This `UiActionDetails` object is sent to the `AlgoBaseUiActionParser`, which instantiates a `UiParser` depending on the `UiActionType` of the object. +The purpose of the `UiParser` is to convert the `UiActionDetails` object into a `UiAction` object. + +The `UiAction` object stores the instructions to make the specified changes to the `Model`, which will run when its `execute` method is called. +The `UiAction` object is passed along to the `UiLogicManager`, which calls its `execute` method on the `Model` object. + +This process is captured by the example in the Sequence Diagrams below. + +.Interaction between `UI` and `UiLogic` +image::gui/EditProblemUiActionSequenceDiagram0.png[] + +Step 1: The user edits the `ProblemDetails` controller class through his/her actions in the GUI. + +Step 2: The `ProblemDetails` class constructs a new `UiActionDetails` object with using `UiActionType.EditProblem`. + +Step 3: The `executeUiAction` of the `MainWindow` class is called with the `UiActionDetails` object, +which in turn calls the `execute` method of `UiLogicManager`. + +Step 4: The method call returns a `UiActionResult` object, which may optionally contain feedback for the user. -The state of the GUI is stored in a `GuiState` object. The `GuiState` object contains a `TabManager` object, which manages tab information such as the tabs that are open and the tabs that are currently selected. +The following diagram goes into more details on how the `UiLogic` handles the `UiActionDetails` + +.Interactions between classes in the `UiLogic` +image::gui/EditProblemUiActionSequenceDiagram1.png[] + +Step 1: The `UiLogicManager` passes the `UiActionDetails` object to the `AlgoBaseActionParser`, +which in turn passes it to the `EditProblemUiActionParser` based on its Action type. + +Step 2: The `EditProblemUiActionParser` converts the `UiActionDetails` object into a `EditProblemUiAction` object, +and passes it back to the `UiLogicManager`. + +Step 3: The `UiLogicManager` executes the `EditProblemUiAction` together with the `Model`, and returns the `CommandResult`. + +===== Design Considerations + +[width="100%",cols="33%,<33%,<33%",options="header",] +|======================================================================= +|Aspect| Alternative 1 (Current Choice) | Alternative 2 +| Handling Commands and UI Actions | +Handle Commands and UI Actions separately. + +**Pros**: Higher modularity. Allows separation the different architectures as well +(Synchronous for Commands & Event-Driven for UI Actions) + +**Cons**: Multiple Logic managers (LogicManager and UiLogicManager) +| +Handle Commands and UI Actions together. + +**Pros**: Less code and higher reusability. + +**Cons**: Higher coupling and less cohesion. +| Handling different kinds of UI Actions | +Using a command structure with a central parser and many smaller parsers. + +**Pros**: Higher extensibility, easier to add new UI Actions + +**Cons**: Have to write more code to achieve the same functionality. +| + +Handling each UI action individually. + +**Pros**: Can write less code to achieve the same functionality. + +**Cons**: Lower extensibility, harder to add new UI Actions +| +|======================================================================= + +==== Tabbing Feature + +===== Current Implementation + +The state of the GUI is stored in a `GuiState` object, which is in turn stored in the `Model`. The `GuiState` object contains a `TabManager` object, which manages tab information such as the tabs that are open and the tabs that are currently selected. The information is linked to the `UI` through listeners. When the attributes of the `TabManager` object is updated, it triggers callback functions in the UI controllers that update the state of the UI displayed. +The following class diagram illustrates how the classes in the `GuiState` interact with one another: + +.Class Dagram for the `GuiState` class +image::design/model/GuiPackageDiagram.png[width='60%'] + The following commands facilitate the management of tabs: -* `AlgoBase#switchTab()` - Switch between tabs within a specified Tab pane. -* `AlgoBase#openTab()` - Opens a new tab containing details of a model. -* `AlgoBase#closeTab()` - Closes an existing tab. +* `switchTab` - Switch between tabs within a specified Tab pane. +* `openTab` - Opens a new tab containing details of a model. +* `closeTab` - Closes an existing tab. These operations are exposed in the `TabManager` class respectively as: -* `SwitchTab()`: `TabManager#setDisplayTabIndex` and `TabManager#setDetailsTabIndex` -* `OpenTab()`: `TabManager#addTab` -* `CloseTab()`: `TabManager#removeTab` +* `SwitchTab`: `TabManager#switchTab` +* `OpenTab`: `TabManager#openTab` +* `CloseTab`: `TabManager#closeTab` The following Activity Diagrams illustrate what happens when the user executes a `SwitchTabCommand` or `OpenTabCommand` +.Activity Diagram for the Execution of `switchtab` Command image::gui/SwitchTabCommandActivityDiagram.png[width="50%"] -_Figure 1.1 Activity Diagram for the Execution of `switchtab` Command_ +.Activity Diagram for the Execution of `opentab` Command image::gui/OpenTabCommandActivityDiagram.png[width="50%"] -_Figure 1.2 Activity Diagram for the Execution of `opentab` Command_ Given below is an example usage scenario and how the tag mechanism behaves at each step. +.Sequence Diagram for instantiating a `SwitchCommand` object image::gui/SwitchTabsSequenceDiagram0.png[] -_Figure 2.1 Sequence Diagram for instantiating a `SwitchCommand` object_ Step 1: The user executes `switchtab tt/display i/1` to switch to the first tab in the `display` tabpane. @@ -549,28 +680,26 @@ Step 2: `SwitchTabCommandParser` processes the user input, retrieving the tab ty Step 3: These two attributes are passed into the constructor of a `SwitchTabCommand` and a corresponding `SwitchTabCommand` object is returned to the LogicManager - +.Sequence Diagram for updating the tab index in the TabManager image::gui/SwitchTabsSequenceDiagram1.png[] -_Figure 2.2 Sequence Diagram for updating the tab index in the TabManager_ Step 4: `LogicManager` invokes `execute()` method of the returned `SwitchTabCommand`, which retrieves the TabManager from the `Model` object. The `setDisplayTabPaneIndex(1)` method is invoked with the index 1 that the `SwitchTabCommand` was instantiated with. Step 5: Invoking this method updates the integer value in the `displayTabIndex` field (type `ObservableIntegerValue`) of the `TabManager`. - +.Sequence Diagram for reflecting the tab changes image::gui/SwitchTabsSequenceDiagram2.png[] -_Figure 3.3 Sequence Diagram for reflecting the tab changes_ Step 6: A listener was added to the `displayTabIndex` field when the application was initialized. When a change in the value is detected, it triggers the `selectTab(1)` method with the value of the new index passed as an argument. This updates the selected tab in the UI. +.Sequence Diagram for storing new GUI state image::gui/SwitchTabsSequenceDiagram3.png[] -_Figure 3.4 Sequence Diagram for storing new GUI state_ Step 7: After the command is executed, the state of the GUI changes. This causes the `StorageManager` to save the modified GUI state as a new `JSON` file. This is done with the help of the `JsonSerializableGui`, `JsonSerializableTabManager` and `JsonSerializableTab` classes that are wrappers for the `GuiState`, `TabManager` and `TabData` classes. These wrapper classes can be converted into `JSON` format for storage without any data loss. -==== Design Considerations +===== Design Considerations [width="100%",cols="33%,<33%,<33%",options="header",] |======================================================================= @@ -602,7 +731,7 @@ Updating the UI synchronously | |======================================================================= -// end::switchTab[] +// end::gui[] //@@author //@@author Seris370 diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index a5fa52918ad..7f753981c17 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -449,35 +449,75 @@ Examples: * `setplan 10` +=== Editing from GUI + +AlgoBase currently supports editing Problems and Plans from the GUI with an intuitive layout. + +==== Editing a Problem / Plan + +.Plan with name "Data Structures" is opened. +image::gui/EditPlanUiAction0.png[width="70%"] + +Step 1: Select the problem / plan you want to delete by double clicking on it. + +.The name and start date of the plan are modified. +image::gui/EditPlanUiAction1.png[width="70%"] + +Step 2: Make changes to the problem / plan by editing the fields directly. + +.The plan is successfully edited. +image::gui/EditPlanUiAction2.png[width="70%"] + +Step 3: Save changes to the problem by clicking on the _"Edit Problem"_ / _"Edit Plan"_ button. + + +==== Deleting a Problem / Plan + +.Problem with name "Sequences" is opened. +image::gui/DeleteProblemUiAction0.png[width="70%"] + +Step 1: Select the problem / plan you want to delete by double clicking on it. + +.Warning Dialog to confirm problem being deleted. +image::gui/DeleteProblemUiAction1.png[width="70%"] + +Step 2: Click on the red _"Delete Problem"_ / _"Delete Plan"_ button at the bottom right. + +Step 3: For _Problems_, select whether you want the problem to be removed from all existing plans. + +.The problem is successfully deleted. +image::gui/DeleteProblemUiAction2.png[width="70%"] + +Step 4: Click on _Confirm_. + === Tabs +There are 2 types of tabs in AlgoBase: Display and Details tabs, as seen in the figure below. + +.Types of tabs highlighted in Orange image::gui/TabsOverview.png[width="70%"] -_fig 3.7.0 – Types of Tabs highlighted in Orange_ -There are 2 types of tabs in AlgoBase: Display and Details tabs. * **Display tabs** give a high level overview of the contents of a list of items -(e.g. a list of `problems`/`tags`/`plans`). +(e.g. a list of _problems_ / _tags_ / _plans_ / _findrules_). * **Details tabs** give a more detailed description of an item in a display tab. -In the example in fig 3.7.0, there is a problem called "Sequences" in the current display tab. -The details for this problem can be seen in the details tab where the orange arrow is pointing to. - ==== Switching Tabs: `switchtab` + +.Demonstration of the `SwitchTab` command image::gui/SwitchTabCommand.png[width="70%"] -_fig 3.7.1 – Demonstration of `SwitchTab` command_ Switches between tabs in the GUI + -Format: `switchtab tt/TAB_TYPE i/TAB_INDEX` +Format: `switchtab tt/TAB_TYPE i/TAB_INDEX` + Format: `st tt/TAB_TYPE i/TAB_INDEX` * Tab Type -** can be `display` or `details` -** Alternatively, `display` and `details` can be replaced by `1` and `2` respectively +** can be **display** or **details** +** Alternatively, **display** and **details** can be replaced by **1** and **2** respectively Examples: -* `switchtab tt/display i/3` – Switches to the third **display** tab (i.e. `plans` tab), as seen in fig 3.7.1 +* `switchtab tt/display i/3` – Switches to the third **display** tab (i.e. _plans_ tab). * `st tt/1 i/3` – Same effects as the previous command but in a shorter format. * `switchtab tt/details i/3` – Switches to the third **details** tab * `st tt/2 i/3` – Same effects as the previous command but in a shorter format. @@ -485,27 +525,28 @@ Examples: ==== Opening Tabs: `opentab` +.Demonstration of the `OpenTab` command image::gui/OpenTabCommand.png[width="70%"] Opens a new **Details** tab in the GUI + -Format: `opentab m/MODEL_TYPE i/MODEL_INDEX` +Format: `opentab m/MODEL_TYPE i/MODEL_INDEX` + Format: `ot m/MODEL_TYPE i/MODEL_INDEX` * Model Type -** can be `problem`, `tag`, `plan` and `findrule` -** Alternatively, `problem`, `tag`, `plan` and `findrule` can be replaced by `1`, `2`, `3` and `4` respectively +** can be _problem_, _tag_, _plan_ and _findrule_ +** Alternatively, _problem_, _tag_, _plan_ and _findrule_ can be replaced by _1_, _2_, _3_ and _4_ respectively Examples: -* `opentab m/problem i/2` – Opens the 2nd problem in the list of problems, as seen in fig 3.7.2 +* `opentab m/problem i/2` – Opens the 2nd problem in the list of problems. * `ot m/1 i/2` – Same effects as the previous command but in a shorter format. -* `opentab m/plan i/3` – Opens the 3nd plan in the list of plans, as seen in fig 3.7.2 +* `opentab m/plan i/3` – Opens the 3nd plan in the list of plans. * `ot m/3 i/3` – Same effects as the previous command but in a shorter format. ==== Closing Tabs: `closetab` Closes a **details** tab in the GUI + -Format: `closetab i/DETAILS_TAB_INDEX` +Format: `closetab i/DETAILS_TAB_INDEX` + Format: `ct i/DETAILS_TAB_INDEX` Examples: diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml index d021b3992ed..1c95d142573 100644 --- a/docs/diagrams/ArchitectureDiagram.puml +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -7,6 +7,7 @@ Package " "<>{ Class UI UI_COLOR Class Logic LOGIC_COLOR + Class UILogic UI_LOGIC_COLOR Class Storage STORAGE_COLOR Class Model MODEL_COLOR Class Main MODEL_COLOR_T1 @@ -27,8 +28,11 @@ HiddenModel -left[hidden]-> Model Main -up-> HiddenUI Main -left-> HiddenModel UI -> Logic +UI -> UILogic UI -right-> Model +UILogic -> Model Logic -> Storage +UILogic -> Storage Logic -down-> Model Logs -right- Commons diff --git a/docs/diagrams/ArchitectureSequenceDiagram1.puml b/docs/diagrams/ArchitectureSequenceDiagram1.puml new file mode 100644 index 00000000000..520c30a6d12 --- /dev/null +++ b/docs/diagrams/ArchitectureSequenceDiagram1.puml @@ -0,0 +1,37 @@ +@startuml +!include style.puml + +Actor User as user USER_COLOR +Participant ":UI" as ui UI_COLOR +Participant ":UiLogic" as uilogic UI_LOGIC_COLOR +Participant ":Model" as model MODEL_COLOR +Participant ":Storage" as storage STORAGE_COLOR + +user -[USER_COLOR]> ui : Switches tab +activate ui UI_COLOR + +ui -[UI_COLOR]> uilogic : execute(uiActionDetails) +activate uilogic UI_LOGIC_COLOR + +uilogic -[UI_LOGIC_COLOR]> model : switchTab(index) +activate model MODEL_COLOR + +model -[MODEL_COLOR]-> uilogic +deactivate model + +uilogic -[UI_LOGIC_COLOR]> storage : saveAlgoBase(algoBase) +activate storage STORAGE_COLOR + +storage -[STORAGE_COLOR]> storage : Save to file +activate storage STORAGE_COLOR_T1 +deactivate storage + +storage --[STORAGE_COLOR]> uilogic +deactivate storage + +uilogic --[UI_LOGIC_COLOR]> ui +deactivate uilogic + +ui--[UI_COLOR]> user +deactivate ui +@enduml diff --git a/docs/diagrams/EditSequenceDiagram.puml b/docs/diagrams/EditSequenceDiagram.puml new file mode 100644 index 00000000000..2bd74654026 --- /dev/null +++ b/docs/diagrams/EditSequenceDiagram.puml @@ -0,0 +1,69 @@ +@startuml +!include style.puml + +box UiLogic UI_LOGIC_COLOR_T1 +participant ":UiLogicManager" as UiLogicManager UI_LOGIC_COLOR +participant ":AlgoBaseUiActionParser" as AlgoBaseUiActionParser UI_LOGIC_COLOR +participant ":EditProblemUiActionParser" as EditProblemUiActionParser UI_LOGIC_COLOR +participant "d:EditProblemUiAction" as EditProblemUiAction UI_LOGIC_COLOR +participant ":UiActionResult" as UiActionResult UI_LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> UiLogicManager : execute(uiActionDetails) +activate UiLogicManager + +UiLogicManager -> AlgoBaseUiActionParser : parseUiAction(uiActionDetails) +activate AlgoBaseUiActionParser + +create EditProblemUiActionParser +AlgoBaseUiActionParser -> EditProblemUiActionParser +activate EditProblemUiActionParser + +EditProblemUiActionParser --> AlgoBaseUiActionParser +deactivate EditProblemUiActionParser + +AlgoBaseUiActionParser -> EditProblemUiActionParser : parse(uiActionDetails) +activate EditProblemUiActionParser + +create EditProblemUiAction +EditProblemUiActionParser -> EditProblemUiAction +activate EditProblemUiAction + +EditProblemUiAction --> EditProblemUiActionParser : d +deactivate EditProblemUiAction + +EditProblemUiActionParser --> AlgoBaseUiActionParser : d +deactivate EditProblemUiActionParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditProblemUiActionParser -[hidden]-> AlgoBaseUiActionParser +destroy EditProblemUiActionParser + +AlgoBaseUiActionParser --> UiLogicManager : d +deactivate AlgoBaseUiActionParser + +UiLogicManager -> EditProblemUiAction : execute(model) +activate EditProblemUiAction + +EditProblemUiAction -> Model : editProblem(Id.generateId("11b")) +activate Model + +Model --> EditProblemUiAction +deactivate Model + +create UiActionResult +EditProblemUiAction -> UiActionResult +activate UiActionResult + +UiActionResult --> EditProblemUiAction +deactivate UiActionResult + +EditProblemUiAction --> UiLogicManager : result +deactivate EditProblemUiAction + +[<--UiLogicManager +deactivate UiLogicManager +@enduml diff --git a/docs/diagrams/UiLogicClassDiagram.puml b/docs/diagrams/UiLogicClassDiagram.puml new file mode 100644 index 00000000000..9cc6d97432d --- /dev/null +++ b/docs/diagrams/UiLogicClassDiagram.puml @@ -0,0 +1,61 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_LOGIC_COLOR_T4 +skinparam classBackgroundColor UI_LOGIC_COLOR + +package Model{ +Class HiddenModel #FFFFFF +} + +package UiLogic { + + package UiAction { + + Class XYZUiAction + Class UiActionResult + Class "{abstract}\nUiAction" as UiAction + package exceptions { + Class UiActionException + } + } + + package Parser { + package exceptions { + Class ParseException + } + Interface UiParser <> + Class UiActionDetails + Enum UiActionType <> + Class AlgoBaseUiActionParser + Class XYZUiActionParser + Class ParserUtil + } + + Interface UiLogic <> + Class UiLogicManager +} + +Class HiddenOutside #FFFFFF +HiddenOutside ..> UiLogic + +UiLogicManager ..|> UiLogic +UiLogicManager -->"1" AlgoBaseUiActionParser +AlgoBaseUiActionParser ..> UiActionDetails: parses > +AlgoBaseUiActionParser ..> XYZUiActionParser: creates > +XYZUiActionParser ..> UiActionDetails: parses > +XYZUiActionParser ..> XYZUiAction : creates > +XYZUiActionParser ..|> UiParser +XYZUiActionParser ..> ParserUtil +UiActionDetails ..> UiActionType : defined by > +XYZUiAction --|> UiAction +UiLogicManager ..> UiAction : executes > + +UiLogicManager --> Model +UiAction ..> Model +note left of XYZUiAction: XYZUiAction = EditPlanUiAction, \nSwitchDetailsTabUiAction, etc +UiLogic ..> UiActionResult +UiLogicManager ..> UiActionResult +UiAction ..> UiActionResult +UiActionResult -[hidden]-> UiParser +@enduml diff --git a/docs/diagrams/gui/EditProblemUiActionSequenceDiagram0.puml b/docs/diagrams/gui/EditProblemUiActionSequenceDiagram0.puml new file mode 100644 index 00000000000..16a5a3a9645 --- /dev/null +++ b/docs/diagrams/gui/EditProblemUiActionSequenceDiagram0.puml @@ -0,0 +1,48 @@ +@startuml +!include ../style.puml + +box UI UI_COLOR_T1 +participant ":ProblemDetails" as ProblemDetails UI_COLOR +participant ":MainWindow" as MainWindow UI_COLOR +participant ":ResultDisplay" as ResultDisplay UI_COLOR +end box + +box UiLogic UI_LOGIC_COLOR_T1 +participant ":UiActionDetails" as UiActionDetails UI_LOGIC_COLOR +participant ":UiLogicManager" as UiLogicManager UI_LOGIC_COLOR +end box + +[-> ProblemDetails : User edits problem in the GUI +activate ProblemDetails + +create UiActionDetails +ProblemDetails -> UiActionDetails : UiActionDetails(UiActionType.EditProblem) +activate UiActionDetails + +UiActionDetails --> ProblemDetails : uiAction +deactivate UiActionDetails + +ProblemDetails -> MainWindow : executeUiAction(uiAction) +activate MainWindow + +MainWindow --> UiLogicManager: execute(uiAction) +activate UiLogicManager + +UiLogicManager --> MainWindow : uiActionResult +deactivate UiLogicManager + +opt There is feedback for user + + MainWindow -> ResultDisplay : sendFeedbackToUser + activate ResultDisplay + + ResultDisplay --> MainWindow + deactivate ResultDisplay +end + +MainWindow --> ProblemDetails +deactivate MainWindow + +[<--ProblemDetails +deactivate ProblemDetails +@enduml diff --git a/docs/diagrams/gui/EditProblemUiActionSequenceDiagram1.puml b/docs/diagrams/gui/EditProblemUiActionSequenceDiagram1.puml new file mode 100644 index 00000000000..4049bbbec69 --- /dev/null +++ b/docs/diagrams/gui/EditProblemUiActionSequenceDiagram1.puml @@ -0,0 +1,69 @@ +@startuml +!include ../style.puml + +box UiLogic UI_LOGIC_COLOR_T1 +participant ":UiLogicManager" as UiLogicManager UI_LOGIC_COLOR +participant ":AlgoBaseUiActionParser" as AlgoBaseUiActionParser UI_LOGIC_COLOR +participant ":EditProblemUiActionParser" as EditProblemUiActionParser UI_LOGIC_COLOR +participant "d:EditProblemUiAction" as EditProblemUiAction UI_LOGIC_COLOR +participant ":UiActionResult" as UiActionResult UI_LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> UiLogicManager : execute(uiActionDetails) +activate UiLogicManager + +UiLogicManager -> AlgoBaseUiActionParser : parseUiAction(uiActionDetails) +activate AlgoBaseUiActionParser + +create EditProblemUiActionParser +AlgoBaseUiActionParser -> EditProblemUiActionParser +activate EditProblemUiActionParser + +EditProblemUiActionParser --> AlgoBaseUiActionParser +deactivate EditProblemUiActionParser + +AlgoBaseUiActionParser -> EditProblemUiActionParser : parse(uiActionDetails) +activate EditProblemUiActionParser + +create EditProblemUiAction +EditProblemUiActionParser -> EditProblemUiAction +activate EditProblemUiAction + +EditProblemUiAction --> EditProblemUiActionParser : d +deactivate EditProblemUiAction + +EditProblemUiActionParser --> AlgoBaseUiActionParser : d +deactivate EditProblemUiActionParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditProblemUiActionParser -[hidden]-> AlgoBaseUiActionParser +destroy EditProblemUiActionParser + +AlgoBaseUiActionParser --> UiLogicManager : d +deactivate AlgoBaseUiActionParser + +UiLogicManager -> EditProblemUiAction : execute(model) +activate EditProblemUiAction + +EditProblemUiAction -> Model : editProblem(Id.generateId("11b")) +activate Model + +Model --> EditProblemUiAction +deactivate Model + +create UiActionResult +EditProblemUiAction -> UiActionResult +activate UiActionResult + +UiActionResult --> EditProblemUiAction +deactivate UiActionResult + +EditProblemUiAction --> UiLogicManager : result +deactivate EditProblemUiAction + +[<--UiLogicManager +deactivate UiLogicManager +@enduml diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index fad8b0adeaa..487d3d1d2ce 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -19,6 +19,12 @@ !define LOGIC_COLOR_T3 #1616B0 !define LOGIC_COLOR_T4 #101086 +!define UI_LOGIC_COLOR #501885 +!define UI_LOGIC_COLOR_T1 #C2ADF8 +!define UI_LOGIC_COLOR_T2 #6A6ADC +!define UI_LOGIC_COLOR_T3 #1616B0 +!define UI_LOGIC_COLOR_T4 #3C2960 + !define MODEL_COLOR #9D0012 !define MODEL_COLOR_T1 #F97181 !define MODEL_COLOR_T2 #E41F36 diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png index aa2d337d932..b520bee565d 100644 Binary files a/docs/images/ArchitectureDiagram.png and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram1.png b/docs/images/ArchitectureSequenceDiagram1.png new file mode 100644 index 00000000000..49a518fb8d9 Binary files /dev/null and b/docs/images/ArchitectureSequenceDiagram1.png differ diff --git a/docs/images/EditSequenceDiagram.png b/docs/images/EditSequenceDiagram.png new file mode 100644 index 00000000000..8ce9b25a0ed Binary files /dev/null and b/docs/images/EditSequenceDiagram.png differ diff --git a/docs/images/UiLogicClassDiagram.png b/docs/images/UiLogicClassDiagram.png new file mode 100644 index 00000000000..7d45f7488f3 Binary files /dev/null and b/docs/images/UiLogicClassDiagram.png differ diff --git a/docs/images/gui/DeleteProblemUiAction0.png b/docs/images/gui/DeleteProblemUiAction0.png new file mode 100644 index 00000000000..c7aeb84d21f Binary files /dev/null and b/docs/images/gui/DeleteProblemUiAction0.png differ diff --git a/docs/images/gui/DeleteProblemUiAction1.png b/docs/images/gui/DeleteProblemUiAction1.png new file mode 100644 index 00000000000..ccd3912a361 Binary files /dev/null and b/docs/images/gui/DeleteProblemUiAction1.png differ diff --git a/docs/images/gui/DeleteProblemUiAction2.png b/docs/images/gui/DeleteProblemUiAction2.png new file mode 100644 index 00000000000..ba8005c3ebb Binary files /dev/null and b/docs/images/gui/DeleteProblemUiAction2.png differ diff --git a/docs/images/gui/EditPlanUiAction0.png b/docs/images/gui/EditPlanUiAction0.png new file mode 100644 index 00000000000..c56c2342919 Binary files /dev/null and b/docs/images/gui/EditPlanUiAction0.png differ diff --git a/docs/images/gui/EditPlanUiAction1.png b/docs/images/gui/EditPlanUiAction1.png new file mode 100644 index 00000000000..72b119b7e97 Binary files /dev/null and b/docs/images/gui/EditPlanUiAction1.png differ diff --git a/docs/images/gui/EditPlanUiAction2.png b/docs/images/gui/EditPlanUiAction2.png new file mode 100644 index 00000000000..574b26d83cd Binary files /dev/null and b/docs/images/gui/EditPlanUiAction2.png differ diff --git a/docs/images/gui/EditProblemUiActionSequenceDiagram0.png b/docs/images/gui/EditProblemUiActionSequenceDiagram0.png new file mode 100644 index 00000000000..324d5c8527f Binary files /dev/null and b/docs/images/gui/EditProblemUiActionSequenceDiagram0.png differ diff --git a/docs/images/gui/EditProblemUiActionSequenceDiagram1.png b/docs/images/gui/EditProblemUiActionSequenceDiagram1.png new file mode 100644 index 00000000000..8ce9b25a0ed Binary files /dev/null and b/docs/images/gui/EditProblemUiActionSequenceDiagram1.png differ diff --git a/docs/images/gui/OpenTabCommand.png b/docs/images/gui/OpenTabCommand.png index 4ed6663fc52..2bbb7fea656 100644 Binary files a/docs/images/gui/OpenTabCommand.png and b/docs/images/gui/OpenTabCommand.png differ diff --git a/docs/images/gui/SwitchTabCommand.png b/docs/images/gui/SwitchTabCommand.png index 848fa8c4b52..a2a7de61621 100644 Binary files a/docs/images/gui/SwitchTabCommand.png and b/docs/images/gui/SwitchTabCommand.png differ diff --git a/docs/images/gui/TabsOverview.png b/docs/images/gui/TabsOverview.png index 58482a282aa..a23189997ec 100644 Binary files a/docs/images/gui/TabsOverview.png and b/docs/images/gui/TabsOverview.png differ diff --git a/docs/team/seris370.adoc b/docs/team/seris370.adoc index 1d11edd3bfa..da8dd29126a 100644 --- a/docs/team/seris370.adoc +++ b/docs/team/seris370.adoc @@ -13,6 +13,9 @@ AlgoBase is a desktop address book application used for teaching Software Engine == Summary of contributions +* *Code contributed*: +https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#=undefined&search=seris370[Code contributed] + * *Major enhancement*: added *the ability to undo/redo previous commands* ** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. ** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. diff --git a/docs/team/tiuweehan.adoc b/docs/team/tiuweehan.adoc index 66253514d81..27ca4d79ce6 100644 --- a/docs/team/tiuweehan.adoc +++ b/docs/team/tiuweehan.adoc @@ -13,6 +13,8 @@ AlgoBase is a desktop address book application used for teaching Software Engine == Summary of contributions +* *Code contributed*: +https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#=undefined&search=tiuweehan[Code contributed] * *Major enhancement*: added *the ability to undo/redo previous commands* ** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. ** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. diff --git a/src/main/java/seedu/algobase/MainApp.java b/src/main/java/seedu/algobase/MainApp.java index b9b59cd6cd3..d6733308ac7 100644 --- a/src/main/java/seedu/algobase/MainApp.java +++ b/src/main/java/seedu/algobase/MainApp.java @@ -30,6 +30,8 @@ import seedu.algobase.storage.UserPrefsStorage; import seedu.algobase.ui.Ui; import seedu.algobase.ui.UiManager; +import seedu.algobase.ui.action.UiLogic; +import seedu.algobase.ui.action.UiLogicManager; /** * Runs the application. @@ -42,6 +44,7 @@ public class MainApp extends Application { protected Ui ui; protected Logic logic; + protected UiLogic uiLogic; protected Storage storage; protected Model model; protected Config config; @@ -67,8 +70,9 @@ public void init() throws Exception { model = initModelManager(storage, userPrefs); logic = new LogicManager(model, storage); + uiLogic = new UiLogicManager(model, storage); - ui = new UiManager(logic); + ui = new UiManager(logic, uiLogic); } /** diff --git a/src/main/java/seedu/algobase/commons/core/Messages.java b/src/main/java/seedu/algobase/commons/core/Messages.java index 474087105c0..0971d4f5273 100644 --- a/src/main/java/seedu/algobase/commons/core/Messages.java +++ b/src/main/java/seedu/algobase/commons/core/Messages.java @@ -5,6 +5,8 @@ */ public class Messages { + public static final String MESSAGE_UNKNOWN_UI_ACTION = "Unknown action"; + public static final String MESSAGE_UNKNOWN_UI_ACTION_PROPERTY = "Unknown action property"; public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_FIND_RULE_DISPLAYED_INDEX = "The Find Rule index provided is invalid"; diff --git a/src/main/java/seedu/algobase/logic/Logic.java b/src/main/java/seedu/algobase/logic/Logic.java index 1bf6f7dec33..fb50cfe118e 100644 --- a/src/main/java/seedu/algobase/logic/Logic.java +++ b/src/main/java/seedu/algobase/logic/Logic.java @@ -16,7 +16,6 @@ import seedu.algobase.model.searchrule.problemsearchrule.ProblemSearchRule; import seedu.algobase.model.tag.Tag; import seedu.algobase.model.task.Task; -import seedu.algobase.storage.SaveStorageRunnable; /** * API of the Logic component @@ -31,13 +30,6 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; - /** - * Returns a runnable that saves the AlgoBase. - * - * @see seedu.algobase.model.Model#getAlgoBase() - */ - SaveStorageRunnable getSaveAlgoBaseStorageRunnable(); - /** * Returns the AlgoBase. * diff --git a/src/main/java/seedu/algobase/logic/LogicManager.java b/src/main/java/seedu/algobase/logic/LogicManager.java index a6c3fd589c7..5746e2c0375 100644 --- a/src/main/java/seedu/algobase/logic/LogicManager.java +++ b/src/main/java/seedu/algobase/logic/LogicManager.java @@ -23,7 +23,6 @@ import seedu.algobase.model.searchrule.problemsearchrule.ProblemSearchRule; import seedu.algobase.model.tag.Tag; import seedu.algobase.model.task.Task; -import seedu.algobase.storage.SaveStorageRunnable; import seedu.algobase.storage.Storage; /** @@ -57,31 +56,19 @@ public CommandResult execute(String commandText) throws CommandException, ParseE history.add(commandText); } - if (!getSaveAlgoBaseStorageRunnable().save()) { - throw new CommandException(FILE_OPS_ERROR_MESSAGE); + try { + storage.saveAlgoBase(model.getAlgoBase()); + } catch (IOException ioe) { + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); } + TabManager tabManager = getGuiState().getTabManager(); tabManager.refreshTabManager(); return commandResult; } - @Override - public SaveStorageRunnable getSaveAlgoBaseStorageRunnable() { - return new SaveStorageRunnable() { - @Override - public boolean save() { - try { - storage.saveAlgoBase(model.getAlgoBase()); - return true; - } catch (IOException ioe) { - return false; - } - } - }; - } - @Override public ReadOnlyAlgoBase getAlgoBase() { return model.getAlgoBase(); diff --git a/src/main/java/seedu/algobase/logic/commands/ClearCommand.java b/src/main/java/seedu/algobase/logic/commands/ClearCommand.java index 8bb546cb205..a876179f949 100644 --- a/src/main/java/seedu/algobase/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/algobase/logic/commands/ClearCommand.java @@ -22,6 +22,7 @@ public class ClearCommand extends Command { @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); + model.clearPlans(); model.setAlgoBase(new AlgoBase()); return new CommandResult(MESSAGE_SUCCESS); } diff --git a/src/main/java/seedu/algobase/logic/commands/gui/OpenTabCommand.java b/src/main/java/seedu/algobase/logic/commands/gui/OpenTabCommand.java index 26d36e61884..dc2bc3c6085 100644 --- a/src/main/java/seedu/algobase/logic/commands/gui/OpenTabCommand.java +++ b/src/main/java/seedu/algobase/logic/commands/gui/OpenTabCommand.java @@ -18,7 +18,7 @@ import seedu.algobase.model.gui.WriteOnlyTabManager; /** - * Close tabs in the GUI. + * Opens a tab in the GUI. */ public class OpenTabCommand extends Command { diff --git a/src/main/java/seedu/algobase/model/AlgoBase.java b/src/main/java/seedu/algobase/model/AlgoBase.java index ea8858dd90c..9d0383b9349 100644 --- a/src/main/java/seedu/algobase/model/AlgoBase.java +++ b/src/main/java/seedu/algobase/model/AlgoBase.java @@ -272,6 +272,13 @@ void removePlan(Plan key) { plans.remove(key); } + /** + * Clear the list of plans + */ + public void clearPlans() { + plans.clear(); + } + @Override public ObservableList getPlanList() { return plans.asUnmodifiableObservableList(); diff --git a/src/main/java/seedu/algobase/model/Model.java b/src/main/java/seedu/algobase/model/Model.java index 624c832a2b8..76f57080252 100644 --- a/src/main/java/seedu/algobase/model/Model.java +++ b/src/main/java/seedu/algobase/model/Model.java @@ -205,6 +205,11 @@ public interface Model { */ void deletePlan(Plan plan); + /** + * Clear the list of plans + */ + void clearPlans(); + /** * Adds the given Plan. * {@code Plan} must not already exist in the algobase. diff --git a/src/main/java/seedu/algobase/model/ModelManager.java b/src/main/java/seedu/algobase/model/ModelManager.java index 18cd4d97a62..d4b54777d9e 100644 --- a/src/main/java/seedu/algobase/model/ModelManager.java +++ b/src/main/java/seedu/algobase/model/ModelManager.java @@ -243,6 +243,11 @@ public void deletePlan(Plan target) { algoBase.removePlan(target); } + @Override + public void clearPlans() { + algoBase.clearPlans(); + } + @Override public void addPlan(Plan plan) { algoBase.addPlan(plan); diff --git a/src/main/java/seedu/algobase/model/gui/TabManager.java b/src/main/java/seedu/algobase/model/gui/TabManager.java index fc72599f017..9b4f5f5cda9 100644 --- a/src/main/java/seedu/algobase/model/gui/TabManager.java +++ b/src/main/java/seedu/algobase/model/gui/TabManager.java @@ -163,7 +163,7 @@ public TabCommandType closeDetailsTab(TabData tabData) throws NoSuchElementExcep this.tabsData.remove(tabData); // If there are no tab data - if (this.tabsData.size() == 0) { + if (this.tabsData.size() == 0 || detailsTabPaneIndexValue == 0) { // Do nothing } else if (detailsTabPaneIndexValue >= tabIndex.getZeroBased()) { // decrement the details tab pane index diff --git a/src/main/java/seedu/algobase/model/plan/PlanList.java b/src/main/java/seedu/algobase/model/plan/PlanList.java index 7bf0eaa50e4..746a4897310 100644 --- a/src/main/java/seedu/algobase/model/plan/PlanList.java +++ b/src/main/java/seedu/algobase/model/plan/PlanList.java @@ -100,6 +100,17 @@ public void remove(Plan toRemove) { clearCurrentPlan(); } + /** + * Removes the equivalent Plan from the list. + * The Plan must exist in the list. + */ + public void clear() { + currentPlan.set(""); + solvedCount.set(0); + unsolvedCount.set(0); + internalTaskList.setAll(); + } + /** * Replaces the contents of this list with {@code replacement}. */ diff --git a/src/main/java/seedu/algobase/storage/SaveStorageRunnable.java b/src/main/java/seedu/algobase/storage/SaveStorageRunnable.java deleted file mode 100644 index 9f69ff7978f..00000000000 --- a/src/main/java/seedu/algobase/storage/SaveStorageRunnable.java +++ /dev/null @@ -1,8 +0,0 @@ -package seedu.algobase.storage; - -/** - * Runnable that saves to storage. - */ -public interface SaveStorageRunnable { - boolean save(); -} diff --git a/src/main/java/seedu/algobase/ui/MainWindow.java b/src/main/java/seedu/algobase/ui/MainWindow.java index 56a5c5ec513..fe4f3adbd0a 100644 --- a/src/main/java/seedu/algobase/ui/MainWindow.java +++ b/src/main/java/seedu/algobase/ui/MainWindow.java @@ -1,5 +1,6 @@ package seedu.algobase.ui; +import java.util.Optional; import java.util.logging.Logger; import javafx.event.ActionEvent; @@ -18,6 +19,9 @@ import seedu.algobase.logic.commands.exceptions.CommandException; import seedu.algobase.logic.parser.exceptions.ParseException; import seedu.algobase.model.ModelType; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionResult; +import seedu.algobase.ui.action.UiLogic; import seedu.algobase.ui.details.DetailsTabPane; import seedu.algobase.ui.display.DisplayTab; import seedu.algobase.ui.display.DisplayTabPane; @@ -34,6 +38,7 @@ public class MainWindow extends UiPart { private Stage primaryStage; private Logic logic; + private UiLogic uiLogic; private ProblemListPanel problemListPanel; private TagListPanel tagListPanel; @@ -60,12 +65,13 @@ public class MainWindow extends UiPart { @FXML private SplitPane layoutPanePlaceholder; - public MainWindow(Stage primaryStage, Logic logic) { + public MainWindow(Stage primaryStage, Logic logic, UiLogic uiLogic) { super(FXML, primaryStage); // Set dependencies this.primaryStage = primaryStage; this.logic = logic; + this.uiLogic = uiLogic; // Configure the UI setWindowDefaultSize(logic.getGuiSettings()); @@ -118,7 +124,7 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { */ void fillInnerParts() { DisplayTabPane displayTabPane = getDisplayTabPane(); - DetailsTabPane detailsTabPane = new DetailsTabPane(logic); + DetailsTabPane detailsTabPane = new DetailsTabPane(logic, this::executeUiAction); TaskManagementPane taskManagementPane = new TaskManagementPane( logic.getProcessedTaskList(), logic.getCurrentPlan(), @@ -146,14 +152,11 @@ void fillInnerParts() { private DisplayTabPane getDisplayTabPane() { problemListPanel = new ProblemListPanel( logic.getProcessedProblemList(), - logic.getGuiState().getTabManager(), - logic.getSaveAlgoBaseStorageRunnable() + this::executeUiAction ); planListPanel = new PlanListPanel( logic.getProcessedPlanList(), - logic.getGuiState().getTabManager(), - logic.getSaveAlgoBaseStorageRunnable(), - logic.getAlgoBase() + this::executeUiAction ); tagListPanel = new TagListPanel(logic.getProcessedTagList()); findRuleListPanel = new FindRuleListPanel(logic.getProcessedFindRuleList()); @@ -162,8 +165,8 @@ private DisplayTabPane getDisplayTabPane() { DisplayTab planListPanelTab = new DisplayTab(ModelType.PLAN.getTabName(), planListPanel); DisplayTab findRuleListPaneTab = new DisplayTab(ModelType.FINDRULE.getTabName(), findRuleListPanel); return new DisplayTabPane( - logic.getGuiState().getTabManager(), - logic.getSaveAlgoBaseStorageRunnable(), + logic.getGuiState().getReadOnlyTabManager(), + this::executeUiAction, problemListPanelTab, tagListPanelTab, planListPanelTab, @@ -238,4 +241,32 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + /** + * Executes a UI action returns the result. + * + * @see seedu.algobase.ui.action.UiLogic#execute(UiActionDetails) + */ + private UiActionResult executeUiAction(UiActionDetails uiActionDetails) { + try { + UiActionResult uiActionResult = uiLogic.execute(uiActionDetails); + uiActionResult.getFeedbackToUser().ifPresent((feedback) -> { + logger.info("Result: " + feedback); + resultDisplay.setFeedbackToUser(feedback); + }); + + if (uiActionResult.isShowHelp()) { + handleHelp(); + } + + if (uiActionResult.isExit()) { + handleExit(); + } + + return uiActionResult; + } catch (UiActionException | ParseException e) { + resultDisplay.setFeedbackToUser(e.getMessage()); + return new UiActionResult(Optional.empty()); + } + } } diff --git a/src/main/java/seedu/algobase/ui/PlanCard.java b/src/main/java/seedu/algobase/ui/PlanCard.java index 1d145980484..c7ed957e8d2 100644 --- a/src/main/java/seedu/algobase/ui/PlanCard.java +++ b/src/main/java/seedu/algobase/ui/PlanCard.java @@ -13,11 +13,10 @@ import seedu.algobase.commons.core.LogsCenter; import seedu.algobase.logic.parser.ParserUtil; import seedu.algobase.model.ModelType; -import seedu.algobase.model.ReadOnlyAlgoBase; -import seedu.algobase.model.gui.TabData; -import seedu.algobase.model.gui.WriteOnlyTabManager; import seedu.algobase.model.plan.Plan; -import seedu.algobase.storage.SaveStorageRunnable; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; /** * An UI component that displays information of a {@code Plan}. @@ -43,13 +42,7 @@ public class PlanCard extends UiPart { @FXML private Label endDate; - public PlanCard( - Plan plan, - int displayedIndex, - WriteOnlyTabManager writeOnlyTabManager, - SaveStorageRunnable saveStorageRunnable, - ReadOnlyAlgoBase algoBase - ) { + public PlanCard(Plan plan, int displayedIndex, UiActionExecutor uiActionExecutor) { super(FXML); this.planIndex = displayedIndex - 1; this.plan = plan; @@ -68,7 +61,7 @@ public PlanCard( endDate.setText(plan.getEndDate().format(ParserUtil.FORMATTER)); endDate.setWrapText(true); endDate.setTextAlignment(TextAlignment.JUSTIFY); - addMouseClickListener(writeOnlyTabManager, saveStorageRunnable, algoBase); + addMouseClickListener(uiActionExecutor); } @Override @@ -93,13 +86,9 @@ public boolean equals(Object other) { /** * Spawns a new Tab when the cardPane registers a double click event. * - * @param writeOnlyTabManager The tab manager to be written to. + * @param uiActionExecutor The executor for the given UI action */ - public void addMouseClickListener( - WriteOnlyTabManager writeOnlyTabManager, - SaveStorageRunnable saveStorageRunnable, - ReadOnlyAlgoBase algoBase - ) { + public void addMouseClickListener(UiActionExecutor uiActionExecutor) { cardPane.setOnMouseClicked(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { @@ -107,9 +96,15 @@ public void handle(MouseEvent mouseEvent) { if (mouseEvent.getClickCount() == 2) { logger.info("Double Clicked on Problem card with name " + plan.getPlanName()); logger.info("Opening new plan tab"); - writeOnlyTabManager.openDetailsTab(new TabData(ModelType.PLAN, plan.getId())); - saveStorageRunnable.save(); - algoBase.setCurrentPlan(planIndex); + uiActionExecutor.execute(new UiActionDetails( + UiActionType.OPEN_DETAILS_TAB, + ModelType.PLAN, + plan.getId() + )); + uiActionExecutor.execute(new UiActionDetails( + UiActionType.SET_PLAN, + plan.getId() + )); } } } diff --git a/src/main/java/seedu/algobase/ui/PlanListPanel.java b/src/main/java/seedu/algobase/ui/PlanListPanel.java index f271d60d84e..173c4c96da5 100644 --- a/src/main/java/seedu/algobase/ui/PlanListPanel.java +++ b/src/main/java/seedu/algobase/ui/PlanListPanel.java @@ -8,11 +8,8 @@ import javafx.scene.control.ListView; import javafx.scene.layout.Region; import seedu.algobase.commons.core.LogsCenter; -import seedu.algobase.model.ReadOnlyAlgoBase; -import seedu.algobase.model.gui.WriteOnlyTabManager; import seedu.algobase.model.plan.Plan; -import seedu.algobase.storage.SaveStorageRunnable; - +import seedu.algobase.ui.action.UiActionExecutor; /** * Panel containing the list of plans. @@ -21,25 +18,16 @@ public class PlanListPanel extends UiPart { private static final String FXML = "PlanListPanel.fxml"; private final Logger logger = LogsCenter.getLogger(PlanListPanel.class); - private final WriteOnlyTabManager writeOnlyTabManager; - private final SaveStorageRunnable saveStorageRunnable; - private final ReadOnlyAlgoBase algoBase; + private final UiActionExecutor uiActionExecutor; @FXML private ListView planListView; - public PlanListPanel( - ObservableList planList, - WriteOnlyTabManager writeOnlyTabManager, - SaveStorageRunnable saveStorageRunnable, - ReadOnlyAlgoBase algoBase - ) { + public PlanListPanel(ObservableList planList, UiActionExecutor uiActionExecutor) { super(FXML); planListView.setItems(planList); planListView.setCellFactory(listView -> new PlanListViewCell()); - this.writeOnlyTabManager = writeOnlyTabManager; - this.saveStorageRunnable = saveStorageRunnable; - this.algoBase = algoBase; + this.uiActionExecutor = uiActionExecutor; } /** @@ -57,9 +45,7 @@ protected void updateItem(Plan plan, boolean empty) { setGraphic(new PlanCard( plan, getIndex() + 1, - writeOnlyTabManager, - saveStorageRunnable, - algoBase + uiActionExecutor ).getRoot()); } } diff --git a/src/main/java/seedu/algobase/ui/ProblemCard.java b/src/main/java/seedu/algobase/ui/ProblemCard.java index 4d7af2960ac..f33c2239d58 100644 --- a/src/main/java/seedu/algobase/ui/ProblemCard.java +++ b/src/main/java/seedu/algobase/ui/ProblemCard.java @@ -14,10 +14,10 @@ import javafx.scene.text.TextAlignment; import seedu.algobase.commons.core.LogsCenter; import seedu.algobase.model.ModelType; -import seedu.algobase.model.gui.TabData; -import seedu.algobase.model.gui.WriteOnlyTabManager; import seedu.algobase.model.problem.Problem; -import seedu.algobase.storage.SaveStorageRunnable; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; /** * An UI component that displays information of a {@code Problem}. @@ -37,6 +37,7 @@ public class ProblemCard extends UiPart { */ public final Problem problem; + private final UiActionExecutor uiActionExecutor; @FXML private HBox cardPane; @@ -59,14 +60,11 @@ public class ProblemCard extends UiPart { @FXML private FlowPane tags; - public ProblemCard( - Problem problem, - int displayedIndex, - WriteOnlyTabManager writeOnlyTabManager, - SaveStorageRunnable saveStorageRunnable - ) { + public ProblemCard(Problem problem, int displayedIndex, UiActionExecutor uiActionExecutor) { super(FXML); this.problem = problem; + this.uiActionExecutor = uiActionExecutor; + id.setText(displayedIndex + ". "); id.setWrapText(true); id.setTextAlignment(TextAlignment.JUSTIFY); @@ -94,7 +92,7 @@ public ProblemCard( problem.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - this.addMouseClickListener(writeOnlyTabManager, saveStorageRunnable); + this.addMouseClickListener(); } @Override @@ -117,21 +115,19 @@ public boolean equals(Object other) { /** * Spawns a new Tab when the cardPane registers a double click event. - * - * @param writeOnlyTabManager the tabManager to be written to. */ - public void addMouseClickListener( - WriteOnlyTabManager writeOnlyTabManager, - SaveStorageRunnable saveStorageRunnable - ) { + public void addMouseClickListener() { cardPane.setOnMouseClicked(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { if (mouseEvent.getButton().equals(MouseButton.PRIMARY)) { if (mouseEvent.getClickCount() == 2) { logger.info("Double Clicked on Problem card with name " + problem.getName()); - writeOnlyTabManager.openDetailsTab(new TabData(ModelType.PROBLEM, problem.getId())); - saveStorageRunnable.save(); + uiActionExecutor.execute(new UiActionDetails( + UiActionType.OPEN_DETAILS_TAB, + ModelType.PROBLEM, + problem.getId() + )); } } } diff --git a/src/main/java/seedu/algobase/ui/ProblemListPanel.java b/src/main/java/seedu/algobase/ui/ProblemListPanel.java index 850e0b0e5b4..6def7c5cbcf 100644 --- a/src/main/java/seedu/algobase/ui/ProblemListPanel.java +++ b/src/main/java/seedu/algobase/ui/ProblemListPanel.java @@ -8,9 +8,8 @@ import javafx.scene.control.ListView; import javafx.scene.layout.Region; import seedu.algobase.commons.core.LogsCenter; -import seedu.algobase.model.gui.WriteOnlyTabManager; import seedu.algobase.model.problem.Problem; -import seedu.algobase.storage.SaveStorageRunnable; +import seedu.algobase.ui.action.UiActionExecutor; /** * Panel containing the list of problems. @@ -19,22 +18,16 @@ public class ProblemListPanel extends UiPart { private static final String FXML = "ProblemListPanel.fxml"; private final Logger logger = LogsCenter.getLogger(ProblemListPanel.class); - private final WriteOnlyTabManager writeOnlyTabManager; - private final SaveStorageRunnable saveStorageRunnable; - @FXML private ListView problemListView; + private final UiActionExecutor uiActionExecutor; - public ProblemListPanel( - ObservableList problemList, - WriteOnlyTabManager writeOnlyTabManager, - SaveStorageRunnable saveStorageRunnable - ) { + public ProblemListPanel(ObservableList problemList, UiActionExecutor uiActionExecutor) { super(FXML); + + this.uiActionExecutor = uiActionExecutor; problemListView.setItems(problemList); problemListView.setCellFactory(listView -> new ProblemListViewCell()); - this.writeOnlyTabManager = writeOnlyTabManager; - this.saveStorageRunnable = saveStorageRunnable; } /** @@ -52,8 +45,7 @@ protected void updateItem(Problem problem, boolean empty) { setGraphic(new ProblemCard( problem, getIndex() + 1, - writeOnlyTabManager, - saveStorageRunnable + uiActionExecutor ).getRoot()); } } diff --git a/src/main/java/seedu/algobase/ui/UiActionException.java b/src/main/java/seedu/algobase/ui/UiActionException.java new file mode 100644 index 00000000000..84f374a2fb9 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/UiActionException.java @@ -0,0 +1,19 @@ +package seedu.algobase.ui; + +import seedu.algobase.ui.action.UiAction; + +/** + * Represents an error which occurs during execution of a {@link UiAction}. + */ +public class UiActionException extends Exception { + public UiActionException(String message) { + super(message); + } + + /** + * Constructs a new {@code UiActionException} with the specified detail {@code message} and {@code cause}. + */ + public UiActionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/seedu/algobase/ui/UiManager.java b/src/main/java/seedu/algobase/ui/UiManager.java index c6b164ea3b8..1652878b234 100644 --- a/src/main/java/seedu/algobase/ui/UiManager.java +++ b/src/main/java/seedu/algobase/ui/UiManager.java @@ -11,6 +11,7 @@ import seedu.algobase.commons.core.LogsCenter; import seedu.algobase.commons.util.StringUtil; import seedu.algobase.logic.Logic; +import seedu.algobase.ui.action.UiLogic; /** * The manager of the UI component. @@ -23,11 +24,13 @@ public class UiManager implements Ui { private static final String ICON_APPLICATION = "/images/algobase.png"; private Logic logic; + private UiLogic uiLogic; private MainWindow mainWindow; - public UiManager(Logic logic) { + public UiManager(Logic logic, UiLogic uiLogic) { super(); this.logic = logic; + this.uiLogic = uiLogic; } @Override @@ -39,7 +42,7 @@ public void start(Stage primaryStage) { primaryStage.setMaximized(true); try { - mainWindow = new MainWindow(primaryStage, logic); + mainWindow = new MainWindow(primaryStage, logic, uiLogic); mainWindow.show(); //This should be called before creating other UI parts mainWindow.fillInnerParts(); diff --git a/src/main/java/seedu/algobase/ui/action/UiAction.java b/src/main/java/seedu/algobase/ui/action/UiAction.java new file mode 100644 index 00000000000..e23852679af --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiAction.java @@ -0,0 +1,24 @@ +package seedu.algobase.ui.action; + +import seedu.algobase.model.Model; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.actions.OpenDetailsTabUiAction; + +/** + * A class representing details of actions that take place in the UI. + */ +public abstract class UiAction { + public static final Class[] UI_ACTION_LIST = { + OpenDetailsTabUiAction.class, + }; + + /** + * Executes the UI Action and optionally returns the result message. + * + * @param model {@code Model} which the command should operate on. + * @return feedback message of the operation result for display + * @throws UiActionException If an error occurs during UI Action execution. + */ + public abstract UiActionResult execute(Model model) throws UiActionException; + +} diff --git a/src/main/java/seedu/algobase/ui/action/UiActionDetails.java b/src/main/java/seedu/algobase/ui/action/UiActionDetails.java new file mode 100644 index 00000000000..3b6564935ec --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiActionDetails.java @@ -0,0 +1,42 @@ +package seedu.algobase.ui.action; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Stores the details of a {@code UiAction}. + */ +public class UiActionDetails { + private final UiActionType actionType; + private final List internalList; + + public UiActionDetails(UiActionType uiActionType, Object... objects) { + this.actionType = uiActionType; + this.internalList = new ArrayList(); + add(objects); + } + + public void add(Object... objects) { + this.internalList.addAll(Arrays.asList(objects)); + } + + public Object get(int index) { + return internalList.get(index); + } + + /** + * Getter for the word of the action. + */ + public UiActionType getActionWord() { + return this.actionType; + } + + /** + * Makes a copy of the current UiActionDetails object. + */ + public UiActionDetails copy() { + UiActionDetails uiActionDetails = new UiActionDetails(actionType, internalList); + return uiActionDetails; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/UiActionExecutor.java b/src/main/java/seedu/algobase/ui/action/UiActionExecutor.java new file mode 100644 index 00000000000..edf765eac4f --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiActionExecutor.java @@ -0,0 +1,14 @@ +package seedu.algobase.ui.action; + +/** + * Represents a function that can execute commands. + */ +@FunctionalInterface +public interface UiActionExecutor { + /** + * Executes the command and returns the result. + * + * @see UiLogic#execute(UiActionDetails) + */ + UiActionResult execute(UiActionDetails uiActionDetails); +} diff --git a/src/main/java/seedu/algobase/ui/action/UiActionResult.java b/src/main/java/seedu/algobase/ui/action/UiActionResult.java new file mode 100644 index 00000000000..a1c94b975e7 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiActionResult.java @@ -0,0 +1,76 @@ +package seedu.algobase.ui.action; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import java.util.Optional; + +/** + * Represents the result of an action. + */ +public class UiActionResult { + + private final Optional feedbackToUser; + + /** Help information should be shown to the user. */ + private final boolean showHelp; + + /** The application should exit. */ + private final boolean exit; + + /** + * Constructs a {@code UiActionResult} with the specified fields. + */ + public UiActionResult(Optional feedbackToUser, boolean showHelp, boolean exit) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = showHelp; + this.exit = exit; + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, + * and other fields set to their default value. + */ + public UiActionResult(Optional feedbackToUser) { + this(feedbackToUser, false, false); + } + + public Optional getFeedbackToUser() { + return feedbackToUser; + } + + public boolean isShowHelp() { + return showHelp; + } + + public boolean isExit() { + return exit; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UiActionResult)) { + return false; + } + + UiActionResult otherUiActionResult = (UiActionResult) other; + return feedbackToUser.equals(otherUiActionResult.feedbackToUser) + && showHelp == otherUiActionResult.showHelp + && exit == otherUiActionResult.exit; + } + + @Override + public int hashCode() { + return Objects.hash(feedbackToUser, showHelp, exit); + } + + @Override + public String toString() { + return "[UiActionResult]: " + feedbackToUser; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/UiActionType.java b/src/main/java/seedu/algobase/ui/action/UiActionType.java new file mode 100644 index 00000000000..d5ed5e2c1d2 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiActionType.java @@ -0,0 +1,16 @@ +package seedu.algobase.ui.action; + +/** + * Types of {@code UiAction}s in AlgoBase. + */ +public enum UiActionType { + SWITCH_DISPLAY_TAB, + OPEN_DETAILS_TAB, + CLOSE_DETAILS_TAB, + SWITCH_DETAILS_TAB, + EDIT_PROBLEM, + DELETE_PROBLEM, + EDIT_PLAN, + DELETE_PLAN, + SET_PLAN +} diff --git a/src/main/java/seedu/algobase/ui/action/UiLogic.java b/src/main/java/seedu/algobase/ui/action/UiLogic.java new file mode 100644 index 00000000000..8ec3edb8234 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiLogic.java @@ -0,0 +1,19 @@ +package seedu.algobase.ui.action; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.ui.UiActionException; + +/** + * Logic for the UI Actions. + */ +public interface UiLogic { + + /** + * Executes the UI Action and returns the result. + * @param uiActionDetails The ui action details as input by the user. + * @return the result of the command execution. + * @throws UiActionException If an error occurs during command execution. + * @throws ParseException If an error occurs during parsing. + */ + UiActionResult execute(UiActionDetails uiActionDetails) throws UiActionException, ParseException; +} diff --git a/src/main/java/seedu/algobase/ui/action/UiLogicManager.java b/src/main/java/seedu/algobase/ui/action/UiLogicManager.java new file mode 100644 index 00000000000..c6e2372e254 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiLogicManager.java @@ -0,0 +1,46 @@ +package seedu.algobase.ui.action; + +import java.io.IOException; +import java.util.logging.Logger; + +import seedu.algobase.commons.core.LogsCenter; +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Model; +import seedu.algobase.storage.Storage; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.parser.AlgoBaseUiActionParser; + +/** + * The main UI LogicManager of the app. + */ +public class UiLogicManager implements UiLogic { + + public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; + + private final Model model; + private final Storage storage; + private final AlgoBaseUiActionParser algoBaseUiActionParser; + + private final Logger logger = LogsCenter.getLogger(UiLogicManager.class); + + + public UiLogicManager(Model model, Storage storage) { + this.model = model; + this.storage = storage; + algoBaseUiActionParser = new AlgoBaseUiActionParser(); + } + + @Override + public UiActionResult execute(UiActionDetails uiActionDetails) throws UiActionException, ParseException { + UiAction uiAction = algoBaseUiActionParser.parseCommand(uiActionDetails); + UiActionResult uiActionResult = uiAction.execute(model); + + try { + storage.saveAlgoBase(model.getAlgoBase()); + } catch (IOException ioe) { + throw new UiActionException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + + return uiActionResult; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/UiParser.java b/src/main/java/seedu/algobase/ui/action/UiParser.java new file mode 100644 index 00000000000..766dde5319f --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/UiParser.java @@ -0,0 +1,15 @@ +package seedu.algobase.ui.action; + +import seedu.algobase.logic.parser.exceptions.ParseException; + +/** + * Represents a Parser that is able to parse user input into a {@code UiAction} of type {@code T}. + */ +public interface UiParser { + + /** + * Parses {@code uiAction} into a command and returns it. + * @throws ParseException if {@code uiAction} does not conform the expected format + */ + T parse(UiActionDetails uiActionDetails) throws ParseException; +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/CloseDetailsTabUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/CloseDetailsTabUiAction.java new file mode 100644 index 00000000000..b89b861d9ec --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/CloseDetailsTabUiAction.java @@ -0,0 +1,46 @@ +package seedu.algobase.ui.action.actions; + +import static seedu.algobase.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.ModelType; +import seedu.algobase.model.gui.TabData; +import seedu.algobase.model.gui.WriteOnlyTabManager; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Closes a tab in the GUI. + */ +public class CloseDetailsTabUiAction extends UiAction { + + public static final String MESSAGE_INVALID_MODEL = "Model does not exist."; + public static final String MESSAGE_INVALID_ELEMENT = "No such tab exists!"; + + private Id modelId; + private ModelType modelType; + + public CloseDetailsTabUiAction(ModelType modelType, Id modelId) { + requireAllNonNull(modelType, modelId); + this.modelType = modelType; + this.modelId = modelId; + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + try { + WriteOnlyTabManager tabManager = model.getGuiState().getTabManager(); + tabManager.closeDetailsTab(new TabData(modelType, modelId)); + return new UiActionResult(Optional.empty()); + } catch (NoSuchElementException exception) { + throw new UiActionException(String.format(MESSAGE_INVALID_ELEMENT)); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException(String.format(MESSAGE_INVALID_MODEL, modelType.getTabName())); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/DeletePlanUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/DeletePlanUiAction.java new file mode 100644 index 00000000000..8abcf6cb0b7 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/DeletePlanUiAction.java @@ -0,0 +1,75 @@ +package seedu.algobase.ui.action.actions; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.plan.Plan; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Deletes an existing Plan in the algobase. + */ +public class DeletePlanUiAction extends UiAction { + + public static final String MESSAGE_DELETE_PLAN_SUCCESS = "Plan [%1$s] deleted."; + + private final Id id; + + /** + * @param id of the Plan in the filtered Plan list to delete + */ + public DeletePlanUiAction(Id id) { + requireNonNull(id); + + this.id = id; + } + + + /** + * Retrieves the plan to be deleted from the problem list. + */ + private Plan retrievePlanToDelete(List planList) throws NoSuchElementException { + for (Plan plan : planList) { + if (plan.getId().equals(id)) { + return plan; + } + } + throw new NoSuchElementException("No such Plan exists"); + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + requireNonNull(model); + List lastShownList = model.getFilteredPlanList(); + + Plan planToDelete = retrievePlanToDelete(lastShownList); + + model.deletePlan(planToDelete); + + return new UiActionResult(Optional.of(String.format(MESSAGE_DELETE_PLAN_SUCCESS, planToDelete.getPlanName()))); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeletePlanUiAction)) { + return false; + } + + // state check + DeletePlanUiAction e = (DeletePlanUiAction) other; + return id.equals(e.id); + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/DeleteProblemUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/DeleteProblemUiAction.java new file mode 100644 index 00000000000..999d64264ee --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/DeleteProblemUiAction.java @@ -0,0 +1,82 @@ +package seedu.algobase.ui.action.actions; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.problem.Problem; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Deletes an existing Problem in the algobase. + */ +public class DeleteProblemUiAction extends UiAction { + + public static final String MESSAGE_DELETE_PROBLEM_SUCCESS = "Problem [%1$s] deleted."; + + private final Id id; + private final boolean isForced; + + /** + * @param id of the Problem in the filtered Problem list to delete + */ + public DeleteProblemUiAction(Id id, boolean isForced) { + requireNonNull(id); + + this.id = id; + this.isForced = isForced; + } + + + /** + * Retrieves the problem to be deleted from the problem list. + */ + private Problem retrieveProblemToDelete(List problemList) throws NoSuchElementException { + for (Problem problem : problemList) { + if (problem.getId().equals(id)) { + return problem; + } + } + throw new NoSuchElementException("No Problem Found"); + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + requireNonNull(model); + List lastShownList = model.getFilteredProblemList(); + + Problem problemToDelete = retrieveProblemToDelete(lastShownList); + + if (model.checkIsProblemUsed(problemToDelete) && isForced) { + model.removeProblemFromAllPlans(problemToDelete); + } + model.deleteProblem(problemToDelete); + + return new UiActionResult( + Optional.of(String.format(MESSAGE_DELETE_PROBLEM_SUCCESS, problemToDelete.getName())) + ); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteProblemUiAction)) { + return false; + } + + // state check + DeleteProblemUiAction e = (DeleteProblemUiAction) other; + return id.equals(e.id); + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/EditPlanUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/EditPlanUiAction.java new file mode 100644 index 00000000000..8f5335bea54 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/EditPlanUiAction.java @@ -0,0 +1,222 @@ +package seedu.algobase.ui.action.actions; + +import static java.util.Objects.requireNonNull; +import static seedu.algobase.model.Model.PREDICATE_SHOW_ALL_PLANS; +import static seedu.algobase.model.searchrule.plansearchrule.TimeRange.ORDER_CONSTRAINTS; +import static seedu.algobase.model.searchrule.plansearchrule.TimeRange.isValidRange; + +import java.time.LocalDate; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; + +import seedu.algobase.commons.util.CollectionUtil; +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.plan.Plan; +import seedu.algobase.model.plan.PlanDescription; +import seedu.algobase.model.plan.PlanName; +import seedu.algobase.model.task.Task; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Edits the details of an existing Plan in the algobase. + */ +public class EditPlanUiAction extends UiAction { + + public static final String MESSAGE_EDIT_PLAN_SUCCESS = "Plan [%1$s] edited."; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_PLAN = "A plan of name [%1$s] already exists in AlgoBase."; + + private final Id id; + private final EditPlanDescriptor editPlanDescriptor; + + /** + * @param id of the Plan in the filtered Plan list to edit + * @param editPlanDescriptor details to edit the Plan with + */ + public EditPlanUiAction(Id id, EditPlanDescriptor editPlanDescriptor) { + requireNonNull(id); + requireNonNull(editPlanDescriptor); + + this.id = id; + this.editPlanDescriptor = new EditPlanDescriptor(editPlanDescriptor); + } + + /** + * Retrieves the plan to be edited from the plan list. + */ + private Plan retrievePlanToEdit(List planList) throws NoSuchElementException { + for (Plan plan : planList) { + if (plan.getId().equals(id)) { + return plan; + } + } + throw new NoSuchElementException("No Plan Found"); + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + requireNonNull(model); + List lastShownList = model.getFilteredPlanList(); + + Plan planToEdit = retrievePlanToEdit(lastShownList); + Plan editedPlan = createEditedPlan(planToEdit, editPlanDescriptor); + + if (!isValidRange(editedPlan.getStartDate(), editedPlan.getEndDate())) { + throw new UiActionException(ORDER_CONSTRAINTS); + } + + if (!planToEdit.isSamePlan(editedPlan) && model.hasPlan(editedPlan)) { + throw new UiActionException(String.format(MESSAGE_DUPLICATE_PLAN, editedPlan.getPlanName())); + } + + model.setPlan(planToEdit, editedPlan); + model.updateFilteredPlanList(PREDICATE_SHOW_ALL_PLANS); + return new UiActionResult(Optional.of(String.format(MESSAGE_EDIT_PLAN_SUCCESS, editedPlan.getPlanName()))); + } + + /** + * Creates and returns a {@code Plan} with the details of {@code planToEdit} + * edited with {@code editPlanDescriptor}. + */ + private static Plan createEditedPlan(Plan planToEdit, EditPlanDescriptor editPlanDescriptor) { + assert planToEdit != null; + + Id id = planToEdit.getId(); + PlanName updatedName = editPlanDescriptor.getPlanName().orElse(planToEdit.getPlanName()); + PlanDescription updatedDescription = editPlanDescriptor.getPlanDescription().orElse( + planToEdit.getPlanDescription()); + LocalDate startDate = editPlanDescriptor.getStartDate().orElse(planToEdit.getStartDate()); + LocalDate endDate = editPlanDescriptor.getEndDate().orElse(planToEdit.getEndDate()); + Set tasks = editPlanDescriptor.getTasks().orElse(planToEdit.getTasks()); + + return new Plan(id, updatedName, updatedDescription, startDate, endDate, tasks); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPlanUiAction)) { + return false; + } + + // state check + EditPlanUiAction e = (EditPlanUiAction) other; + return id.equals(e.id) + && editPlanDescriptor.equals(e.editPlanDescriptor); + } + + /** + * Stores the details to edit the Plan with. Each non-empty field value will replace the + * corresponding field value of the Plan. + */ + public static class EditPlanDescriptor { + private PlanName name; + private PlanDescription description; + private LocalDate startDate; + private LocalDate endDate; + private Set tasks; + public EditPlanDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditPlanDescriptor(EditPlanDescriptor toCopy) { + setPlanName(toCopy.name); + setPlanDescription(toCopy.description); + setStartDate(toCopy.startDate); + setEndDate(toCopy.endDate); + setTasks(toCopy.tasks); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, description, startDate, endDate, tasks); + } + + public void setPlanName(PlanName name) { + this.name = name; + } + + public Optional getPlanName() { + return Optional.ofNullable(name); + } + + public void setPlanDescription(PlanDescription description) { + this.description = description; + } + + public Optional getPlanDescription() { + return Optional.ofNullable(description); + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public Optional getStartDate() { + return Optional.ofNullable(startDate); + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public Optional getEndDate() { + return Optional.ofNullable(endDate); + } + + /** + * Sets {@code tasks} to this object's {@code tasks}. + * A defensive copy of {@code tasks} is used internally. + */ + public void setTasks(Set tasks) { + this.tasks = (tasks != null) ? new HashSet<>(tasks) : null; + } + + /** + * Returns an unmodifiable task set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tasks} is null. + */ + public Optional> getTasks() { + return (tasks != null) ? Optional.of(Collections.unmodifiableSet(tasks)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPlanDescriptor)) { + return false; + } + + // state check + EditPlanDescriptor e = (EditPlanDescriptor) other; + + return getPlanName().equals(e.getPlanName()) + && getPlanDescription().equals(e.getPlanDescription()) + && getStartDate().equals(e.getStartDate()) + && getEndDate().equals(e.getEndDate()) + && getTasks().equals(e.getTasks()); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/EditProblemUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/EditProblemUiAction.java new file mode 100644 index 00000000000..4c7463624a1 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/EditProblemUiAction.java @@ -0,0 +1,263 @@ +package seedu.algobase.ui.action.actions; + +import static java.util.Objects.requireNonNull; +import static seedu.algobase.model.Model.PREDICATE_SHOW_ALL_PROBLEMS; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; + +import seedu.algobase.commons.util.CollectionUtil; +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.problem.Author; +import seedu.algobase.model.problem.Description; +import seedu.algobase.model.problem.Difficulty; +import seedu.algobase.model.problem.Name; +import seedu.algobase.model.problem.Problem; +import seedu.algobase.model.problem.Remark; +import seedu.algobase.model.problem.Source; +import seedu.algobase.model.problem.WebLink; +import seedu.algobase.model.tag.Tag; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Edits the details of an existing Problem in the algobase. + */ +public class EditProblemUiAction extends UiAction { + + public static final String MESSAGE_EDIT_PROBLEM_SUCCESS = "Problem [%1$s] edited."; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_PROBLEM = "Problem [%1$s] already exists in AlgoBase."; + + private final Id id; + private final EditProblemDescriptor editProblemDescriptor; + /** + * @param id of the Problem in the filtered Problem list to edit + * @param editProblemDescriptor details to edit the Problem with + */ + public EditProblemUiAction(Id id, EditProblemDescriptor editProblemDescriptor) { + requireNonNull(id); + requireNonNull(editProblemDescriptor); + + this.id = id; + this.editProblemDescriptor = new EditProblemDescriptor(editProblemDescriptor); + } + + + /** + * Retrieves the problem to be edited from the problem list. + */ + private Problem retrieveProblemToEdit(List problemList) throws NoSuchElementException { + for (Problem problem : problemList) { + if (problem.getId().equals(id)) { + return problem; + } + } + throw new NoSuchElementException("No Problem Found"); + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + requireNonNull(model); + List lastShownList = model.getFilteredProblemList(); + + Problem problemToEdit = retrieveProblemToEdit(lastShownList); + Problem editedProblem = createEditedProblem(problemToEdit, editProblemDescriptor); + + if (!problemToEdit.isSameProblem(editedProblem) && model.hasProblem(editedProblem)) { + throw new UiActionException(String.format(MESSAGE_DUPLICATE_PROBLEM, problemToEdit.getName())); + } + + model.setProblem(problemToEdit, editedProblem); + + if (editProblemDescriptor.tags != null) { + for (Tag tag : editProblemDescriptor.tags) { + if (!model.hasTag(tag)) { + model.addTag(tag); + } + } + } + + model.updateFilteredProblemList(PREDICATE_SHOW_ALL_PROBLEMS); + return new UiActionResult(Optional.of(String.format(MESSAGE_EDIT_PROBLEM_SUCCESS, editedProblem.getName()))); + } + + /** + * Creates and returns a {@code Problem} with the details of {@code problemToEdit} + * edited with {@code editProblemDescriptor}. + */ + private static Problem createEditedProblem(Problem problemToEdit, EditProblemDescriptor editProblemDescriptor) { + assert problemToEdit != null; + + Id id = problemToEdit.getId(); + Name updatedName = editProblemDescriptor.getName().orElse(problemToEdit.getName()); + Author updatedAuthor = editProblemDescriptor.getAuthor().orElse(problemToEdit.getAuthor()); + WebLink updatedWebLink = editProblemDescriptor.getWebLink().orElse(problemToEdit.getWebLink()); + Description updatedDescription = editProblemDescriptor.getDescription().orElse(problemToEdit.getDescription()); + Set updatedTags = editProblemDescriptor.getTags().orElse(problemToEdit.getTags()); + Difficulty updatedDifficulty = editProblemDescriptor.getDifficulty().orElse(problemToEdit.getDifficulty()); + Remark updatedRemark = editProblemDescriptor.getRemark().orElse(problemToEdit.getRemark()); + Source updatedSource = editProblemDescriptor.getSource().orElse(problemToEdit.getSource()); + + return new Problem(id, updatedName, updatedAuthor, updatedWebLink, updatedDescription, updatedTags, + updatedDifficulty, updatedRemark, updatedSource); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof seedu.algobase.logic.commands.problem.EditCommand)) { + return false; + } + + // state check + EditProblemUiAction e = (EditProblemUiAction) other; + return id.equals(e.id) + && editProblemDescriptor.equals(e.editProblemDescriptor); + } + + /** + * Stores the details to edit the Problem with. Each non-empty field value will replace the + * corresponding field value of the Problem. + */ + public static class EditProblemDescriptor { + private Name name; + private Author author; + private WebLink webLink; + private Description description; + private Set tags; + private Difficulty difficulty; + private Remark remark; + private Source source; + + public EditProblemDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditProblemDescriptor(EditProblemDescriptor toCopy) { + setName(toCopy.name); + setAuthor(toCopy.author); + setWebLink(toCopy.webLink); + setDescription(toCopy.description); + setTags(toCopy.tags); + setDifficulty(toCopy.difficulty); + setRemark(toCopy.remark); + setSource(toCopy.source); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, author, webLink, description, tags, difficulty, remark, source); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setAuthor(Author author) { + this.author = author; + } + + public Optional getAuthor() { + return Optional.ofNullable(author); + } + + public void setWebLink(WebLink webLink) { + this.webLink = webLink; + } + + public Optional getWebLink() { + return Optional.ofNullable(webLink); + } + + public void setDescription(Description description) { + this.description = description; + } + + public Optional getDescription() { + return Optional.ofNullable(description); + } + + public void setDifficulty(Difficulty difficulty) { + this.difficulty = difficulty; + } + + public Optional getDifficulty() { + return Optional.ofNullable(difficulty); + } + + public void setRemark(Remark remark) { + this.remark = remark; + } + + public Optional getRemark() { + return Optional.ofNullable(remark); + } + + public void setSource(Source source) { + this.source = source; + } + + public Optional getSource() { + return Optional.ofNullable(source); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof seedu.algobase.logic.commands.problem.EditCommand.EditProblemDescriptor)) { + return false; + } + + // state check + EditProblemDescriptor e = (EditProblemDescriptor) other; + + return getName().equals(e.getName()) + && getAuthor().equals(e.getAuthor()) + && getWebLink().equals(e.getWebLink()) + && getDescription().equals(e.getDescription()) + && getTags().equals(e.getTags()); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/OpenDetailsTabUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/OpenDetailsTabUiAction.java new file mode 100644 index 00000000000..87dfdf798f5 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/OpenDetailsTabUiAction.java @@ -0,0 +1,48 @@ +package seedu.algobase.ui.action.actions; + +import static seedu.algobase.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.ModelType; +import seedu.algobase.model.gui.TabCommandType; +import seedu.algobase.model.gui.TabData; +import seedu.algobase.model.gui.WriteOnlyTabManager; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Opens a tab in the GUI. + */ +public class OpenDetailsTabUiAction extends UiAction { + + public static final String MESSAGE_INVALID_MODEL = "Model does not exist."; + public static final String MESSAGE_INVALID_ELEMENT = "No such tab exists!"; + + private Id modelId; + private ModelType modelType; + + public OpenDetailsTabUiAction(ModelType modelType, Id modelId) { + requireAllNonNull(modelType, modelId); + this.modelType = modelType; + this.modelId = modelId; + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + WriteOnlyTabManager tabManager = model.getGuiState().getTabManager(); + try { + TabData tabData = new TabData(modelType, modelId); + TabCommandType result = tabManager.openDetailsTab(tabData); + return new UiActionResult(Optional.empty()); + } catch (NoSuchElementException exception) { + throw new UiActionException(String.format(MESSAGE_INVALID_ELEMENT)); + } catch (IllegalArgumentException exception) { + throw new IllegalArgumentException(String.format(MESSAGE_INVALID_MODEL, modelType.getTabName())); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/SetPlanUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/SetPlanUiAction.java new file mode 100644 index 00000000000..a24c6defb9e --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/SetPlanUiAction.java @@ -0,0 +1,50 @@ +package seedu.algobase.ui.action.actions; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Optional; + +import seedu.algobase.model.Id; +import seedu.algobase.model.Model; +import seedu.algobase.model.plan.Plan; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Set the plan to be displayed in TaskManagementPane. + */ +public class SetPlanUiAction extends UiAction { + + public static final String MESSAGE_SET_PLAN_FAILURE = "No such plan exists!"; + + private final Id planId; + + public SetPlanUiAction(Id planId) { + this.planId = planId; + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + requireNonNull(model); + + List lastShownList = model.getFilteredPlanList(); + + for (Plan plan : lastShownList) { + if (plan.getId().equals(planId)) { + model.setCurrentPlan(plan); + return new UiActionResult(Optional.empty()); + } + } + + throw new UiActionException(MESSAGE_SET_PLAN_FAILURE); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SetPlanUiAction // instanceof handles nulls + && planId.equals(((SetPlanUiAction) other).planId)); // state check + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/SwitchDetailsTabUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/SwitchDetailsTabUiAction.java new file mode 100644 index 00000000000..eccf81bfb90 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/SwitchDetailsTabUiAction.java @@ -0,0 +1,39 @@ +package seedu.algobase.ui.action.actions; + +import static seedu.algobase.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import seedu.algobase.commons.core.index.Index; +import seedu.algobase.model.Model; +import seedu.algobase.model.gui.WriteOnlyTabManager; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Switches tab in the GUI. + */ +public class SwitchDetailsTabUiAction extends UiAction { + + public static final String MESSAGE_INVALID_TAB_INDEX = "Tab at index [%1$s] does not exist."; + + private Index modelIndex; + + public SwitchDetailsTabUiAction(Index modelIndex) { + requireAllNonNull(modelIndex); + this.modelIndex = modelIndex; + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + try { + WriteOnlyTabManager tabManager = model.getGuiState().getTabManager(); + tabManager.switchDetailsTab(modelIndex); + return new UiActionResult(Optional.empty()); + } catch (NoSuchElementException exception) { + throw new UiActionException(String.format(MESSAGE_INVALID_TAB_INDEX, modelIndex.getOneBased())); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/actions/SwitchDisplayTabUiAction.java b/src/main/java/seedu/algobase/ui/action/actions/SwitchDisplayTabUiAction.java new file mode 100644 index 00000000000..ea75ae85ae8 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/actions/SwitchDisplayTabUiAction.java @@ -0,0 +1,39 @@ +package seedu.algobase.ui.action.actions; + +import static seedu.algobase.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.NoSuchElementException; +import java.util.Optional; + +import seedu.algobase.commons.core.index.Index; +import seedu.algobase.model.Model; +import seedu.algobase.model.gui.WriteOnlyTabManager; +import seedu.algobase.ui.UiActionException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionResult; + +/** + * Switches tab in the GUI. + */ +public class SwitchDisplayTabUiAction extends UiAction { + + public static final String MESSAGE_INVALID_TAB_INDEX = "Tab at index [%1$s] does not exist."; + + private Index modelIndex; + + public SwitchDisplayTabUiAction(Index modelIndex) { + requireAllNonNull(modelIndex); + this.modelIndex = modelIndex; + } + + @Override + public UiActionResult execute(Model model) throws UiActionException { + try { + WriteOnlyTabManager tabManager = model.getGuiState().getTabManager(); + tabManager.switchDisplayTab(modelIndex); + return new UiActionResult(Optional.empty()); + } catch (NoSuchElementException exception) { + throw new UiActionException(String.format(MESSAGE_INVALID_TAB_INDEX, modelIndex.getOneBased())); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/AlgoBaseUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/AlgoBaseUiActionParser.java new file mode 100644 index 00000000000..176684b033f --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/AlgoBaseUiActionParser.java @@ -0,0 +1,52 @@ +package seedu.algobase.ui.action.parser; + +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.ui.action.UiAction; +import seedu.algobase.ui.action.UiActionDetails; + +/** + * Parses UI Actions. + */ +public class AlgoBaseUiActionParser { + + /** + * Parses user input into UI Action for execution. + * + * @param uiActionDetails uiActionDetails object. + * @return the command based on the user input. + * @throws ParseException if the uiAction does not conform the expected format + */ + public UiAction parseCommand(UiActionDetails uiActionDetails) throws ParseException { + switch (uiActionDetails.getActionWord()) { + + // Tabs + case SWITCH_DISPLAY_TAB: + return new SwitchDisplayTabUiActionParser().parse(uiActionDetails); + case OPEN_DETAILS_TAB: + return new OpenDetailsTabUiActionParser().parse(uiActionDetails); + case CLOSE_DETAILS_TAB: + return new CloseDetailsTabUiActionParser().parse(uiActionDetails); + case SWITCH_DETAILS_TAB: + return new SwitchDetailsTabUiActionParser().parse(uiActionDetails); + + // Problems + case EDIT_PROBLEM: + return new EditProblemUiActionParser().parse(uiActionDetails); + case DELETE_PROBLEM: + return new DeleteProblemUiActionParser().parse(uiActionDetails); + + // Plans + case EDIT_PLAN: + return new EditPlanUiActionParser().parse(uiActionDetails); + case DELETE_PLAN: + return new DeletePlanUiActionParser().parse(uiActionDetails); + case SET_PLAN: + return new SetPlanUiActionParser().parse(uiActionDetails); + + default: + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION); + } + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/CloseDetailsTabUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/CloseDetailsTabUiActionParser.java new file mode 100644 index 00000000000..2346529ba8a --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/CloseDetailsTabUiActionParser.java @@ -0,0 +1,49 @@ +package seedu.algobase.ui.action.parser; + +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.model.ModelType; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.CloseDetailsTabUiAction; + +/** + * Parses input arguments and creates a new CloseDetailsTabUiAction object + */ +public class CloseDetailsTabUiActionParser implements UiParser { + + @Override + public CloseDetailsTabUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + ModelType modelType = parseModelType(uiActionDetails.get(0)); + Id id = parseId(uiActionDetails.get(1)); + return new CloseDetailsTabUiAction(modelType, id); + } + + /** + * Converts a modelType of type {@Object} into a modelType of type {@ModelType} + * + * @throws ParseException if given object is not of type {@ModelType} + */ + private ModelType parseModelType(Object modelType) throws ParseException { + if (!(modelType instanceof ModelType)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (ModelType) modelType; + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/DeletePlanUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/DeletePlanUiActionParser.java new file mode 100644 index 00000000000..2e9a4ffec0b --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/DeletePlanUiActionParser.java @@ -0,0 +1,44 @@ +package seedu.algobase.ui.action.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.DeletePlanUiAction; + +/** + * Parses input arguments and creates a new DeletePlanUiAction object + */ +public class DeletePlanUiActionParser implements UiParser { + + private static final int ID_INDEX = 0; + + /** + * Parses the given {@code UiActionDetails} object + * and returns an DeletePlanUiAction object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeletePlanUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + requireNonNull(uiActionDetails); + + Id id = parseId(uiActionDetails.get(ID_INDEX)); + + return new DeletePlanUiAction(id); + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/DeleteProblemUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/DeleteProblemUiActionParser.java new file mode 100644 index 00000000000..ca8605fe49c --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/DeleteProblemUiActionParser.java @@ -0,0 +1,59 @@ +package seedu.algobase.ui.action.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.DeleteProblemUiAction; + +/** + * Parses input arguments and creates a new DeleteProblemUiAction object + */ +public class DeleteProblemUiActionParser implements UiParser { + + private static final int ID_INDEX = 0; + private static final int IS_FORCED_INDEX = 1; + + /** + * Parses the given {@code UiActionDetails} object + * and returns an DeleteProblemUiAction object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteProblemUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + requireNonNull(uiActionDetails); + + Id id = parseId(uiActionDetails.get(ID_INDEX)); + Boolean isForced = parseBoolean(uiActionDetails.get(IS_FORCED_INDEX)); + + return new DeleteProblemUiAction(id, isForced); + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } + + /** + * Converts a boolean of type {@Object} into a boolean of type {@Boolean} + * + * @throws ParseException if given object is not of type {@Boolean} + */ + private Boolean parseBoolean(Object bool) throws ParseException { + if (!(bool instanceof Boolean)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Boolean) bool; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/EditPlanUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/EditPlanUiActionParser.java new file mode 100644 index 00000000000..47115190ccf --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/EditPlanUiActionParser.java @@ -0,0 +1,100 @@ +package seedu.algobase.ui.action.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import java.time.LocalDate; + +import seedu.algobase.logic.parser.ParserUtil; +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.EditPlanUiAction; +import seedu.algobase.ui.action.actions.EditPlanUiAction.EditPlanDescriptor; + +/** + * Parses input arguments and creates a new EditPlanUiAction object + */ +public class EditPlanUiActionParser implements UiParser { + + private static final int ID_INDEX = 0; + private static final int PLAN_NAME_INDEX = 1; + private static final int PLAN_DESCRIPTION_INDEX = 2; + private static final int START_DATE_INDEX = 3; + private static final int END_DATE_INDEX = 4; + + /** + * Parses the given {@code UiActionDetails} object + * and returns an EditPlanUiAction object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditPlanUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + requireNonNull(uiActionDetails); + + Id id = parseId(uiActionDetails.get(ID_INDEX)); + + EditPlanDescriptor editPlanDescriptor = new EditPlanDescriptor(); + + String planNameString = parseString(uiActionDetails.get(PLAN_NAME_INDEX)); + if (!planNameString.isBlank()) { + editPlanDescriptor.setPlanName(ParserUtil.parsePlanName(planNameString)); + } + + String planDescriptionString = parseString(uiActionDetails.get(PLAN_DESCRIPTION_INDEX)); + if (!planDescriptionString.isBlank()) { + editPlanDescriptor.setPlanDescription((ParserUtil.parsePlanDescription(planDescriptionString))); + } + + LocalDate startDate = parseDate(uiActionDetails.get(START_DATE_INDEX)); + editPlanDescriptor.setStartDate(startDate); + + LocalDate endDate = parseDate(uiActionDetails.get(END_DATE_INDEX)); + editPlanDescriptor.setEndDate(endDate); + + if (!editPlanDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditPlanUiAction.MESSAGE_NOT_EDITED); + } + + return new EditPlanUiAction(id, editPlanDescriptor); + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } + + /** + * Converts an date of type {@Object} into a date of type {@LocalDate} + * + * @throws ParseException if given object is not of type {@LocalDate} + */ + private LocalDate parseDate(Object date) throws ParseException { + if (!(date instanceof LocalDate)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (LocalDate) date; + } + + /** + * Converts a string of type {@Object} into an string of type {@String} + * + * @throws ParseException if given object is not of type {@String} + */ + private String parseString(Object string) throws ParseException { + if (!(string instanceof String)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return ((String) string).trim(); + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/EditProblemUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/EditProblemUiActionParser.java new file mode 100644 index 00000000000..7785a69de59 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/EditProblemUiActionParser.java @@ -0,0 +1,127 @@ +package seedu.algobase.ui.action.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.algobase.logic.parser.ParserUtil; +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.model.tag.Tag; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.EditProblemUiAction; +import seedu.algobase.ui.action.actions.EditProblemUiAction.EditProblemDescriptor; + +/** + * Parses input arguments and creates a new EditProblemUiAction object + */ +public class EditProblemUiActionParser implements UiParser { + + private static final int ID_INDEX = 0; + private static final int NAME_INDEX = 1; + private static final int AUTHOR_INDEX = 2; + private static final int WEBLINK_INDEX = 3; + private static final int DESCRIPTION_INDEX = 4; + private static final int DIFFICULTY_INDEX = 5; + private static final int REMARK_INDEX = 6; + private static final int SOURCE_INDEX = 7; + + /** + * Parses the given {@code UiActionDetails} object + * and returns an EditProblemUiAction object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditProblemUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + requireNonNull(uiActionDetails); + + Id id = parseId(uiActionDetails.get(ID_INDEX)); + + EditProblemDescriptor editProblemDescriptor = new EditProblemDescriptor(); + + String nameString = parseString(uiActionDetails.get(NAME_INDEX)); + if (!nameString.isBlank()) { + editProblemDescriptor.setName(ParserUtil.parseName(nameString)); + } + + String authorString = parseString(uiActionDetails.get(AUTHOR_INDEX)); + if (!authorString.isBlank()) { + editProblemDescriptor.setAuthor(ParserUtil.parseAuthor(authorString)); + } + + String weblinkString = parseString(uiActionDetails.get(WEBLINK_INDEX)); + if (!weblinkString.isBlank()) { + editProblemDescriptor.setWebLink(ParserUtil.parseWeblink(weblinkString)); + } + + String descriptionString = parseString(uiActionDetails.get(DESCRIPTION_INDEX)); + if (!descriptionString.isBlank()) { + editProblemDescriptor.setDescription(ParserUtil.parseDescription(descriptionString)); + } + + String difficultyString = parseString(uiActionDetails.get(DIFFICULTY_INDEX)); + if (!difficultyString.isBlank()) { + editProblemDescriptor.setDescription(ParserUtil.parseDescription(descriptionString)); + } + + String remarkString = parseString(uiActionDetails.get(REMARK_INDEX)); + if (!remarkString.isBlank()) { + editProblemDescriptor.setRemark(ParserUtil.parseRemark(remarkString)); + } + String sourceString = parseString(uiActionDetails.get(SOURCE_INDEX)); + if (!sourceString.isBlank()) { + editProblemDescriptor.setSource(ParserUtil.parseSource(sourceString)); + } + + if (!editProblemDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditProblemUiAction.MESSAGE_NOT_EDITED); + } + + return new EditProblemUiAction(id, editProblemDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTagsForEdit(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } + + /** + * Converts a string of type {@Object} into an string of type {@String} + * + * @throws ParseException if given object is not of type {@String} + */ + private String parseString(Object string) throws ParseException { + if (!(string instanceof String)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return ((String) string).trim(); + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/OpenDetailsTabUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/OpenDetailsTabUiActionParser.java new file mode 100644 index 00000000000..e1c55dce920 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/OpenDetailsTabUiActionParser.java @@ -0,0 +1,49 @@ +package seedu.algobase.ui.action.parser; + +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.model.ModelType; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.OpenDetailsTabUiAction; + +/** + * Parses input arguments and creates a new OpenDetailsTabUiAction object + */ +public class OpenDetailsTabUiActionParser implements UiParser { + + @Override + public OpenDetailsTabUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + ModelType modelType = parseModelType(uiActionDetails.get(0)); + Id id = parseId(uiActionDetails.get(1)); + return new OpenDetailsTabUiAction(modelType, id); + } + + /** + * Converts a modelType of type {@Object} into a modelType of type {@ModelType} + * + * @throws ParseException if given object is not of type {@ModelType} + */ + private ModelType parseModelType(Object modelType) throws ParseException { + if (!(modelType instanceof ModelType)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (ModelType) modelType; + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/SetPlanUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/SetPlanUiActionParser.java new file mode 100644 index 00000000000..3ef0eaf82d7 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/SetPlanUiActionParser.java @@ -0,0 +1,35 @@ +package seedu.algobase.ui.action.parser; + +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.model.Id; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.SetPlanUiAction; + + +/** + * Parses input arguments and creates a new SetPlanUiAction object + */ +public class SetPlanUiActionParser implements UiParser { + + @Override + public SetPlanUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + Id id = parseId(uiActionDetails.get(0)); + return new SetPlanUiAction(id); + } + + /** + * Converts an id of type {@Object} into an id of type {@Id} + * + * @throws ParseException if given object is not of type {@Id} + */ + private Id parseId(Object id) throws ParseException { + if (!(id instanceof Id)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Id) id; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/SwitchDetailsTabUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/SwitchDetailsTabUiActionParser.java new file mode 100644 index 00000000000..ceca68cf9a3 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/SwitchDetailsTabUiActionParser.java @@ -0,0 +1,34 @@ +package seedu.algobase.ui.action.parser; + +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.commons.core.index.Index; +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.SwitchDetailsTabUiAction; + +/** + * Parses input arguments and creates a new SwitchDetailsTabUiAction object + */ +public class SwitchDetailsTabUiActionParser implements UiParser { + + @Override + public SwitchDetailsTabUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + Index index = parseIndex(uiActionDetails.get(0)); + return new SwitchDetailsTabUiAction(index); + } + + /** + * Converts an index of type {@Object} into an id of type {@Index} + * + * @throws ParseException if given object is not of type {@Index} + */ + private Index parseIndex(Object index) throws ParseException { + if (!(index instanceof Index)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Index) index; + } +} diff --git a/src/main/java/seedu/algobase/ui/action/parser/SwitchDisplayTabUiActionParser.java b/src/main/java/seedu/algobase/ui/action/parser/SwitchDisplayTabUiActionParser.java new file mode 100644 index 00000000000..c64cd9312d6 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/action/parser/SwitchDisplayTabUiActionParser.java @@ -0,0 +1,34 @@ +package seedu.algobase.ui.action.parser; + +import static seedu.algobase.commons.core.Messages.MESSAGE_UNKNOWN_UI_ACTION_PROPERTY; + +import seedu.algobase.commons.core.index.Index; +import seedu.algobase.logic.parser.exceptions.ParseException; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiParser; +import seedu.algobase.ui.action.actions.SwitchDisplayTabUiAction; + +/** + * UI Parser for the Switch Display Tab Action. + */ +public class SwitchDisplayTabUiActionParser implements UiParser { + + @Override + public SwitchDisplayTabUiAction parse(UiActionDetails uiActionDetails) throws ParseException { + Index index = parseIndex(uiActionDetails.get(0)); + return new SwitchDisplayTabUiAction(index); + } + + /** + * Converts an index of type {@Object} into an id of type {@Index} + * + * @throws ParseException if given object is not of type {@Index} + */ + private Index parseIndex(Object index) throws ParseException { + if (!(index instanceof Index)) { + throw new ParseException(MESSAGE_UNKNOWN_UI_ACTION_PROPERTY); + } + + return (Index) index; + } +} diff --git a/src/main/java/seedu/algobase/ui/details/DetailsTab.java b/src/main/java/seedu/algobase/ui/details/DetailsTab.java index b469cc05c34..349d33c33e3 100644 --- a/src/main/java/seedu/algobase/ui/details/DetailsTab.java +++ b/src/main/java/seedu/algobase/ui/details/DetailsTab.java @@ -5,9 +5,10 @@ import javafx.scene.layout.Region; import seedu.algobase.model.Id; import seedu.algobase.model.ModelType; -import seedu.algobase.model.gui.TabData; -import seedu.algobase.model.gui.WriteOnlyTabManager; import seedu.algobase.ui.UiPart; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; /** * An UI component that displays tab content. @@ -34,14 +35,14 @@ public DetailsTab( UiPart uiPart, ModelType modelType, Id modelId, - WriteOnlyTabManager writeOnlyTabManager + UiActionExecutor uiActionExecutor ) { super(FXML); tabContentPlaceholder = new Tab(name, uiPart.getRoot()); this.modelId = modelId; this.modelType = modelType; - addOnCloseRequestListener(writeOnlyTabManager); + addOnCloseRequestListener(uiActionExecutor); } public Tab getTab() { @@ -51,9 +52,13 @@ public Tab getTab() { /** * Adds an listener that registers when the tab closes. */ - private void addOnCloseRequestListener(WriteOnlyTabManager writeOnlyTabManager) { + private void addOnCloseRequestListener(UiActionExecutor uiActionExecutor) { tabContentPlaceholder.setOnCloseRequest(e -> { - writeOnlyTabManager.closeDetailsTab(new TabData(modelType, modelId)); + uiActionExecutor.execute(new UiActionDetails( + UiActionType.CLOSE_DETAILS_TAB, + modelType, + modelId + )); e.consume(); }); } diff --git a/src/main/java/seedu/algobase/ui/details/DetailsTabPane.java b/src/main/java/seedu/algobase/ui/details/DetailsTabPane.java index f053e7c6d5a..397a40252d4 100644 --- a/src/main/java/seedu/algobase/ui/details/DetailsTabPane.java +++ b/src/main/java/seedu/algobase/ui/details/DetailsTabPane.java @@ -19,12 +19,13 @@ import seedu.algobase.model.ReadOnlyAlgoBase; import seedu.algobase.model.gui.ReadOnlyTabManager; import seedu.algobase.model.gui.TabData; -import seedu.algobase.model.gui.WriteOnlyTabManager; import seedu.algobase.model.plan.Plan; import seedu.algobase.model.problem.Problem; import seedu.algobase.model.tag.Tag; -import seedu.algobase.storage.SaveStorageRunnable; import seedu.algobase.ui.UiPart; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; /** * Contains details about a specific model. @@ -35,20 +36,19 @@ public class DetailsTabPane extends UiPart { private final ReadOnlyAlgoBase algoBase; private final ReadOnlyTabManager readOnlyTabManager; - private final WriteOnlyTabManager writeOnlyTabManager; - private final SaveStorageRunnable saveStorageRunnable; + private final UiActionExecutor uiActionExecutor; @FXML private TabPane tabsPlaceholder; - public DetailsTabPane(Logic logic) { + public DetailsTabPane(Logic logic, UiActionExecutor uiActionExecutor) { super(FXML); + this.uiActionExecutor = uiActionExecutor; + tabsPlaceholder.setTabClosingPolicy(TabPane.TabClosingPolicy.SELECTED_TAB); this.algoBase = logic.getAlgoBase(); this.readOnlyTabManager = logic.getGuiState().getTabManager(); - this.writeOnlyTabManager = logic.getGuiState().getTabManager(); - this.saveStorageRunnable = logic.getSaveAlgoBaseStorageRunnable(); addTabsToTabPane(readOnlyTabManager.getTabsDataList()); if (!readOnlyTabManager.getTabsDataList().isEmpty()) { @@ -57,7 +57,7 @@ public DetailsTabPane(Logic logic) { addListenerForTabChanges(); addListenerForIndexChange(readOnlyTabManager.getDetailsTabPaneIndex()); - addListenerToTabPaneIndexChange(writeOnlyTabManager); + addListenerToTabPaneIndexChange(); } /** @@ -94,16 +94,16 @@ private void addListenerForIndexChange(ObservableIntegerValue detailsTabPaneInde /** * Adds an index change listener to the tab pane. - * - * @param tabManager The TabManager to be modified. */ - private void addListenerToTabPaneIndexChange(WriteOnlyTabManager tabManager) { + private void addListenerToTabPaneIndexChange() { this.tabsPlaceholder.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { if (newValue.intValue() >= 0) { - tabManager.switchDetailsTab(Index.fromZeroBased(newValue.intValue())); - saveStorageRunnable.save(); + uiActionExecutor.execute(new UiActionDetails( + UiActionType.SWITCH_DETAILS_TAB, + Index.fromZeroBased(newValue.intValue()) + )); } } }); @@ -142,10 +142,10 @@ private Optional convertTabDataToDetailsTab(TabData tabData) throws return Optional.of( new DetailsTab( problem.getName().fullName, - new ProblemDetails(problem), + new ProblemDetails(problem, uiActionExecutor), modelType, modelId, - writeOnlyTabManager + uiActionExecutor ) ); case PLAN: @@ -153,10 +153,10 @@ private Optional convertTabDataToDetailsTab(TabData tabData) throws return Optional.of( new DetailsTab( plan.getPlanName().fullName, - new PlanDetails(plan), + new PlanDetails(plan, uiActionExecutor), modelType, modelId, - writeOnlyTabManager + uiActionExecutor ) ); case TAG: diff --git a/src/main/java/seedu/algobase/ui/details/PlanDetails.java b/src/main/java/seedu/algobase/ui/details/PlanDetails.java index 3dc9ac49c96..d11adb1fd1b 100644 --- a/src/main/java/seedu/algobase/ui/details/PlanDetails.java +++ b/src/main/java/seedu/algobase/ui/details/PlanDetails.java @@ -1,11 +1,17 @@ package seedu.algobase.ui.details; import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.DatePicker; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.Region; +import seedu.algobase.model.ModelType; import seedu.algobase.model.plan.Plan; import seedu.algobase.ui.UiPart; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; /** * An UI component that displays detailed information of a {@code Problem}. @@ -19,18 +25,92 @@ public class PlanDetails extends UiPart { @FXML private TextField planName; @FXML - private TextField startDate; + private DatePicker startDate; @FXML - private TextField endDate; + private DatePicker endDate; @FXML private TextArea planDescription; + @FXML + private Button editButton; + @FXML + private Button deleteButton; + @FXML + private WarningDialog warningDialog; - public PlanDetails(Plan plan) { + public PlanDetails(Plan plan, UiActionExecutor uiActionExecutor) { super(FXML); this.plan = plan; + + editButton.setDisable(true); + planName.setText(plan.getPlanName().fullName); - startDate.setText(plan.getStartDate().toString()); - endDate.setText(plan.getEndDate().toString()); + planName.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + + startDate.setValue(plan.getStartDate()); + startDate.valueProperty().addListener((e) -> { + editButton.setDisable(false); + }); + + endDate.setValue(plan.getEndDate()); + endDate.valueProperty().addListener((e) -> { + editButton.setDisable(false); + }); + planDescription.setText(plan.getPlanDescription().value); + planDescription.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + + editButton.setOnMouseClicked((e) -> { + uiActionExecutor.execute(new UiActionDetails( + UiActionType.EDIT_PLAN, + plan.getId(), + planName.getText(), + planDescription.getText(), + startDate.getValue(), + endDate.getValue() + )); + editButton.setDisable(true); + e.consume(); + }); + + this.warningDialog = new WarningDialog( + "Are you sure you want to delete this plan?", (Object... objects) -> { + + boolean shouldDelete = (boolean) objects[0]; + + // Close the warning dialog + if (warningDialog.isShowing()) { + warningDialog.hide(); + } + + if (!shouldDelete) { + return; + } + + // Close the tab + uiActionExecutor.execute(new UiActionDetails( + UiActionType.CLOSE_DETAILS_TAB, + ModelType.PLAN, + plan.getId() + )); + + // Delete the plan + uiActionExecutor.execute(new UiActionDetails( + UiActionType.DELETE_PLAN, + plan.getId() + )); + }); + + deleteButton.setOnMouseClicked((e) -> { + if (!warningDialog.isShowing()) { + warningDialog.show(); + } else { + warningDialog.focus(); + } + e.consume(); + }); } } diff --git a/src/main/java/seedu/algobase/ui/details/ProblemDetails.java b/src/main/java/seedu/algobase/ui/details/ProblemDetails.java index 30e78a80a8d..efc4040a581 100644 --- a/src/main/java/seedu/algobase/ui/details/ProblemDetails.java +++ b/src/main/java/seedu/algobase/ui/details/ProblemDetails.java @@ -1,11 +1,16 @@ package seedu.algobase.ui.details; import javafx.fxml.FXML; +import javafx.scene.control.Button; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.Region; +import seedu.algobase.model.ModelType; import seedu.algobase.model.problem.Problem; import seedu.algobase.ui.UiPart; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; /** * An UI component that displays detailed information of a {@code Problem}. @@ -30,16 +35,108 @@ public class ProblemDetails extends UiPart { private TextField source; @FXML private TextArea description; + @FXML + private Button editButton; + @FXML + private Button deleteButton; + @FXML + private WarningDialog warningDialog; - public ProblemDetails(Problem problem) { + public ProblemDetails(Problem problem, UiActionExecutor uiActionExecutor) { super(FXML); this.problem = problem; + + editButton.setDisable(true); + name.setText(problem.getName().fullName); + name.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + author.setText(problem.getAuthor().value); + author.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + weblink.setText(problem.getWebLink().value); + weblink.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + description.setText(problem.getDescription().value); + description.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + difficulty.setText(problem.getDifficulty().toString()); + difficulty.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + remark.setText(problem.getRemark().value); + remark.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + source.setText(problem.getSource().value); + source.textProperty().addListener((e) -> { + editButton.setDisable(false); + }); + + editButton.setOnMouseClicked((e) -> { + uiActionExecutor.execute(new UiActionDetails( + UiActionType.EDIT_PROBLEM, + problem.getId(), + name.getText(), + author.getText(), + weblink.getText(), + description.getText(), + difficulty.getText(), + remark.getText(), + source.getText() + )); + editButton.setDisable(true); + e.consume(); + }); + + this.warningDialog = new WarningDialog( + "Are you sure you want to delete this problem?", + "Delete this problem from existing tasks", (Object... objects) -> { + + boolean shouldDelete = (boolean) objects[0]; + boolean isForcedDelete = (boolean) objects[1]; + + // Close the warning dialog + if (warningDialog.isShowing()) { + warningDialog.hide(); + } + + if (!shouldDelete) { + return; + } + + // Close the tab + uiActionExecutor.execute(new UiActionDetails( + UiActionType.CLOSE_DETAILS_TAB, + ModelType.PROBLEM, + problem.getId() + )); + + // Delete the problem + uiActionExecutor.execute(new UiActionDetails( + UiActionType.DELETE_PROBLEM, + problem.getId(), + isForcedDelete + )); + }); + + deleteButton.setOnMouseClicked((e) -> { + if (!warningDialog.isShowing()) { + warningDialog.show(); + } else { + warningDialog.focus(); + } + e.consume(); + }); } } diff --git a/src/main/java/seedu/algobase/ui/details/WarningDialog.java b/src/main/java/seedu/algobase/ui/details/WarningDialog.java new file mode 100644 index 00000000000..b6b30890d57 --- /dev/null +++ b/src/main/java/seedu/algobase/ui/details/WarningDialog.java @@ -0,0 +1,116 @@ +package seedu.algobase.ui.details; + +import java.util.function.Consumer; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.stage.Stage; +import seedu.algobase.commons.core.LogsCenter; +import seedu.algobase.ui.UiPart; + +/** + * Controller for the warning dialog. + */ +public class WarningDialog extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(WarningDialog.class); + private static final String FXML = "WarningDialog.fxml"; + private final Consumer callback; + + @FXML + private Button confirmButton; + @FXML + private Button cancelButton; + @FXML + private CheckBox checkbox; + @FXML + private Label warningMessage; + + /** + * Creates a new Warning Dialog + * + * @param root Stage to use as the root of the WarningDialog + */ + public WarningDialog(Stage root, String message, Consumer callback) { + super(FXML, root); + warningMessage.setText(message); + checkbox.setVisible(false); + this.callback = callback; + } + + /** + * Creates a new Warning Dialog with a checkbox. + * + * @param root Stage to use as the root of the HelpWindow. + */ + public WarningDialog(Stage root, String message, String checkboxMessage, Consumer callback) { + super(FXML, root); + warningMessage.setText(message); + checkbox.setText(checkboxMessage); + this.callback = callback; + } + + /** + * Creates a new WarningDialog. + */ + public WarningDialog(String message, Consumer callback) { + this(new Stage(), message, callback); + } + + + /** + * Creates a new WarningDialog with a checkbox. + */ + public WarningDialog(String message, String checkboxMessage, Consumer callback) { + this(new Stage(), message, checkboxMessage, callback); + } + + /** + * Shows the warning dialog.. + */ + void show() { + logger.fine("Opening a Warning Dialog to confirm delete"); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the warning dialog is currently being shown. + */ + boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the warning Dialog. + */ + void hide() { + getRoot().hide(); + } + + /** + * Focuses on the warning Dialog. + */ + void focus() { + getRoot().requestFocus(); + } + + /** + * Confirms the delete and triggers the callback function. + */ + @FXML + private void confirm() { + callback.accept(new Object[] { true, checkbox.isSelected() }); + } + + /** + * Cancels the delete and triggers the callback function. + */ + @FXML + private void cancel() { + callback.accept(new Object[] { false, false }); + } +} diff --git a/src/main/java/seedu/algobase/ui/display/DisplayTabPane.java b/src/main/java/seedu/algobase/ui/display/DisplayTabPane.java index bf588f63c6a..8edaaa167fd 100644 --- a/src/main/java/seedu/algobase/ui/display/DisplayTabPane.java +++ b/src/main/java/seedu/algobase/ui/display/DisplayTabPane.java @@ -1,5 +1,7 @@ package seedu.algobase.ui.display; +import static java.util.Objects.requireNonNull; + import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableIntegerValue; import javafx.beans.value.ObservableValue; @@ -7,10 +9,12 @@ import javafx.scene.control.TabPane; import javafx.scene.layout.Region; import seedu.algobase.commons.core.index.Index; -import seedu.algobase.model.gui.TabManager; -import seedu.algobase.model.gui.WriteOnlyTabManager; -import seedu.algobase.storage.SaveStorageRunnable; +import seedu.algobase.model.gui.ReadOnlyTabManager; import seedu.algobase.ui.UiPart; +import seedu.algobase.ui.action.UiActionDetails; +import seedu.algobase.ui.action.UiActionExecutor; +import seedu.algobase.ui.action.UiActionType; + /** * Pane containing the different tabs. @@ -18,18 +22,21 @@ public class DisplayTabPane extends UiPart { private static final String FXML = "DisplayTabPane.fxml"; + private final UiActionExecutor uiActionExecutor; @FXML private TabPane tabsPlaceholder; - public DisplayTabPane(TabManager tabManager, SaveStorageRunnable saveStorageRunnable, DisplayTab... displayTabs) { + public DisplayTabPane(ReadOnlyTabManager tabManager, UiActionExecutor uiActionExecutor, DisplayTab... displayTabs) { super(FXML); + requireNonNull(uiActionExecutor); + + this.uiActionExecutor = uiActionExecutor; addTabsToTabPane(displayTabs); selectTab(tabManager.getDisplayTabPaneIndex().getValue().intValue()); addListenerForIndexChange(tabManager.getDisplayTabPaneIndex()); - addListenerToTabPaneIndexChange(tabManager, saveStorageRunnable); } /** @@ -56,16 +63,15 @@ private void addListenerForIndexChange(ObservableIntegerValue displayTabPaneInde /** * Adds an index change listener to the tab pane. - * - * @param tabManager A callback function for when the index of the tabPane changes. */ - private void addListenerToTabPaneIndexChange( - WriteOnlyTabManager tabManager, SaveStorageRunnable saveStorageRunnable) { + private void addListenerToTabPaneIndexChange() { this.tabsPlaceholder.getSelectionModel().selectedIndexProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { - tabManager.switchDisplayTab(Index.fromZeroBased(newValue.intValue())); - saveStorageRunnable.save(); + uiActionExecutor.execute(new UiActionDetails( + UiActionType.SWITCH_DISPLAY_TAB, + Index.fromZeroBased(newValue.intValue()) + )); } }); } diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 679324c28c2..acc86d5164d 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -163,6 +163,30 @@ -fx-text-fill: #B0E0E6; } +.edit_button { + -fx-padding: 5 22 5 22; + -fx-border-width: 2; + -fx-border-color: #105f9f; + -fx-background-radius: 0; + -fx-background-color: #105f9f; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #d8d8d8; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + +.delete_button { + -fx-padding: 5 22 5 22; + -fx-border-width: 2; + -fx-border-color: #871f1f; + -fx-background-radius: 0; + -fx-background-color: #871f1f; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #d8d8d8; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } @@ -244,15 +268,6 @@ * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ */ .button { - -fx-padding: 5 22 5 22; - -fx-border-color: #e2e2e2; - -fx-border-width: 2; - -fx-background-radius: 0; - -fx-background-color: #1d1d1d; - -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; - -fx-font-size: 11pt; - -fx-text-fill: #d8d8d8; - -fx-background-insets: 0 0 0 0, 0, 1, 2; } .button:hover { diff --git a/src/main/resources/view/PlanDetails.fxml b/src/main/resources/view/PlanDetails.fxml index ebae1f431e1..3d1e5597121 100644 --- a/src/main/resources/view/PlanDetails.fxml +++ b/src/main/resources/view/PlanDetails.fxml @@ -5,11 +5,14 @@ + + + AnchorPane.rightAnchor="0.0" xmlns:fx="http://javafx.com/fxml/1" styleClass="edge-to-edge" + fitToWidth="true"> @@ -23,30 +26,26 @@