diff --git a/.gitignore b/.gitignore index 823d175eb670..fab3c222d020 100644 --- a/.gitignore +++ b/.gitignore @@ -8,13 +8,17 @@ lib/* *.log *.log.* *.csv -config.json +config/ +/config.json +gradle/ src/test/data/sandbox/ preferences.json .DS_Store ./screenshot*.png classes/ +cache/ /data/ /bin/ src/main/resources/docs/ out/ +.gitattributes diff --git a/README.adoc b/README.adoc index 450054624f48..b63354dc2f74 100644 --- a/README.adoc +++ b/README.adoc @@ -1,40 +1,47 @@ -= Address Book (Level 4) += Piconso: The Console-based Image Editor + ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] -https://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] +https://travis-ci.org/CS2103-AY1819S1-T09-3/main[image:https://travis-ci.org/CS2103-AY1819S1-T09-3/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/benedictcss/main[image:https://ci.appveyor.com/api/projects/status/9odxi2csp68dsqm7?svg=true[Build Status]] +https://coveralls.io/github/CS2103-AY1819S1-T09-3/main?branch=master[image:https://coveralls.io/repos/github/CS2103-AY1819S1-T09-3/main/badge.svg?branch=master[Coverage Status]] +image:https://api.codacy.com/project/badge/Grade/6e5a3a4804ff4ce08fa5d3358ed0d67a[link="https://app.codacy.com/app/lancelotwillow/main?utm_source=github.com&utm_medium=referral&utm_content=CS2103-AY1819S1-T09-3/main&utm_campaign=Badge_Grade_Dashboard"] + +Piconso is an CLI-based (Command Line Interface) Image Editor designed and developed for quick and efficient image-editing. Many image-editors are complex and difficult to pick up, coupled with a steep learning curve, and sometimes even a steep price! + + +Unlike them, Piconso offers the same features, in a much more simplified manner. With Piconso, all traditional image-editing features can be performed solely through text commands, removing the hassle of working with a mouse. The interface can be used by +photographers, designers, developers and more, and is very easy to pick up. + +== Product ifdef::env-github[] -image::docs/images/Ui.png[width="600"] +image::docs/images/Ui.png[width="602"] endif::[] ifndef::env-github[] -image::images/Ui.png[width="600"] +image::images/Ui.png[width="602"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +Above shows the UI layout + +* *Upper left column*: Shows history of transformations performed on current layer. +* *Lower left column*: Shows first 10 images in current directory. +* *Middle*: Shows original image and rendered preview. +* *Right column*: Layers currently used. +* *Bottom*: Command output console and input for commands. + +== Features +* Several traditional image-editing features such as crop, blur etc. +* Capability to define own custom transformation set. +* Direct access to images on Google Photos (download and upload). +* A wide range of edit text-commands that remove the need for a mouse. == Site Map * <> * <> -* <> * <> -* <> == Acknowledgements -* Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by -_Marco Jakob_. -* Libraries used: https://github.com/TestFX/TestFX[TextFX], https://bitbucket.org/controlsfx/controlsfx/[ControlsFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5] - -== Licence : link:LICENSE[MIT] +* This application is morphed from the https://github.com/se-edu/[AddressBook-Level4] project created by _SE-EDU_ initiative for student learning. diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..7106c9369bbb --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,32 @@ +{ + "ignoreGlobList": ["**.adoc", "collate**", "**resources/imageMagic**"], + "formats": ["gradle", "jade", "java", "js", "md", "scss", "yml", "xml"], + "authors": + [ + { + "githubId": "j-lum", + "displayName": "Jeffry", + "authorNames": ["j-lum", "Jeffry Lum"] + }, + { + "githubId": "benedictcss", + "displayName": "Benedict", + "authorNames": ["benedictcss", "DESKTOP-56O063T\\Benedict"] + }, + { + "githubId": "chivent", + "displayName": "Kai Le", + "authorNames": ["Ong Kai Le"] + }, + { + "githubId": "ihwk1996", + "displayName": "Ivan", + "authorNames": ["Ivan Ho Weng Kwong"] + }, + { + "githubId": "lancelotwillow", + "displayName": "Tianyang", + "authorNames": ["ZHANG TIANYANG"] + } + ] +} diff --git a/build.gradle b/build.gradle index f8e614f8b49b..34fd40bc7545 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,12 @@ dependencies { String testFxVersion = '4.0.12-alpha' String jUnitVersion = '5.1.0' + compile 'com.google.photos.library:google-photos-library-client:1.0.0' + compile 'com.google.apis:google-api-services-plus:v1-rev557-1.25.0' + compile 'com.google.oauth-client:google-oauth-client-java6:1.23.0' + compile 'com.google.oauth-client:google-oauth-client-jetty:1.23.0' + compile 'com.google.api-client:google-api-client:1.25.0' + compile 'io.grpc:grpc-netty:1.13.2' implementation group: 'org.controlsfx', name: 'controlsfx', version: '8.40.11' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' @@ -82,7 +88,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'piconso.jar' destinationDir = file("${buildDir}/jar/") } @@ -207,8 +213,8 @@ asciidoctor { idprefix: '', // for compatibility with GitHub preview idseparator: '-', 'site-root': "${sourceDir}", // must be the same as sourceDir, do not modify - 'site-name': 'AddressBook-Level4', - 'site-githuburl': 'https://github.com/se-edu/addressbook-level4', + 'site-name': 'Piconso', + 'site-githuburl': 'https://cs2103-ay1819s1-t09-3.github.io/main/index.html', 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..94f2a5034099 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,51 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + +Piconso was developed by the CS2103-AY1819S1-T09-3. + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Lum Ka Fai Jeffry +image::j-lum.png[width="150", align="left"] +{empty}[https://github.com/j-lum[github]] [<>] -Role: Project Advisor +Role: Team Lead + +Responsibilities: Layers, Canvas and UI ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Ivan Ho Weng Kwong +image::ihwk1996.png[width="150", align="left"] +{empty}[http://github.com/ihwk1996[github]] [<>] Role: Team Lead + -Responsibilities: UI +Responsibilities: Model, Image State Management (undo, redo) ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Benedict Chua Song Shing +image::benedictcss.png[width="150", align="left"] +{empty}[http://github.com/benedictcss[github]] [<>] Role: Developer + -Responsibilities: Data +Responsibilities: Storage, Project Management, Directory Navigation ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Zhang Tianyang +image::lancelotwillow.png[width="150", align="left"] +{empty}[http://github.com/lancelotwillow[github]] [<>] Role: Developer + -Responsibilities: Dev Ops + Threading +Responsibilities: ImageMagick (image transformation) ''' - -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Ong Kai Le +image::chivent.png[width="150", align="left"] +{empty}[http://github.com/chivent[github]] [<>] Role: Developer + -Responsibilities: UI +Responsibilities: UI, Google Connectivity ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..52b49c6222cb 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,6 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103-AY1819S1-T09-3/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] * *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 817ec81d7832..31346d874469 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += Piconso - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -13,9 +13,9 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103-AY1819S1-T09-3/main -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `CS2103-AY1819S1-T09-3`      Since: `Aug 2018`      Licence: `MIT` == Setting up @@ -46,11 +46,11 @@ Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plu . Click `Open as Project` . Click `OK` to accept the default settings . Open a console and run the command `gradlew processResources` (Mac/Linux: `./gradlew processResources`). It should finish with the `BUILD SUCCESSFUL` message. + -This will generate all resources required by the application and tests. -. Open link:{repoURL}/src/main/java/seedu/address/storage/XmlAdaptedPerson.java[`XmlAdaptedPerson.java`] and link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow.java`] and check for any code errors -.. Due to an ongoing https://youtrack.jetbrains.com/issue/IDEA-189060[issue] with some of the newer versions of IntelliJ, code errors may be detected even if the project can be built and run successfully -.. To resolve this, place your cursor over any of the code section highlighted in red. Press kbd:[ALT + ENTER], and select `Add '--add-modules=...' to module compiler options` for each error -. Repeat this for the test folder as well (e.g. check link:{repoURL}/src/test/java/seedu/address/commons/util/XmlUtilTest.java[`XmlUtilTest.java`] and link:{repoURL}/src/test/java/seedu/address/ui/HelpWindowTest.java[`HelpWindowTest.java`] for code errors, and if so, resolve it the same way) +This will generate all resources required by the application and tests. + +. Open MainWindow.java and other files to check for any code errors + +.. Due to an ongoing issue with some of the newer versions of IntelliJ, code errors may be detected even if the project can be built and run successfully + +.. To resolve this, place your cursor over any of the code section highlighted in red. Press ALT+ENTER, and select Add '--add-modules=…​' to module compiler options for each error + +. Repeat this for the test folder as well (e.g. check HelpWindowTest.java and other files for code errors, and if so, resolve it the same way) === Verifying the setup @@ -72,36 +72,9 @@ This project follows https://github.com/oss-generic/process/blob/master/docs/Cod Optionally, you can follow the <> document to configure Intellij to check style-compliance as you write code. -==== Updating documentation to match your fork - -After forking the repo, the documentation will still have the SE-EDU branding and refer to the `se-edu/addressbook-level4` repo. - -If you plan to develop this fork as a separate product (i.e. instead of contributing to `se-edu/addressbook-level4`), you should do the following: - -. Configure the <> in link:{repoURL}/build.gradle[`build.gradle`], such as the `site-name`, to suit your own project. - -. Replace the URL in the attribute `repoURL` in link:{repoURL}/docs/DeveloperGuide.adoc[`DeveloperGuide.adoc`] and link:{repoURL}/docs/UserGuide.adoc[`UserGuide.adoc`] with the URL of your fork. - -==== Setting up CI - -Set up Travis to perform Continuous Integration (CI) for your fork. See <> to learn how to set it up. - -After setting up Travis, you can optionally set up coverage reporting for your team fork (see <>). - -[NOTE] -Coverage reporting could be useful for a team repository that hosts the final version but it is not that useful for your personal fork. - -Optionally, you can set up AppVeyor as a second CI (see <>). - -[NOTE] -Having both Travis and AppVeyor ensures your App works on both Unix-based platforms and Windows-based platforms (Travis is Unix-based and AppVeyor is Windows-based) - ==== Getting started with coding -When you are ready to start coding, - -1. Get some sense of the overall design by reading <>. -2. Take a look at <>. +When you are ready to start coding, get some sense of the overall design by reading <>. == Design @@ -138,90 +111,65 @@ Each of the four components * Defines its _API_ in an `interface` with the same name as the Component. * Exposes its functionality using a `{Component Name}Manager` class. -For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class. +For example, the `Logic` component (see the class diagram given below) defines it’s API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class. .Class Diagram of the Logic Component -image::LogicClassDiagram.png[width="800"] - -[discrete] -==== Events-Driven nature of the design - -The _Sequence Diagram_ below shows how the components interact for the scenario where the user issues the command `delete 1`. - -.Component interactions for `delete 1` command (part 1) -image::SDforDeletePerson.png[width="800"] - -[NOTE] -Note how the `Model` simply raises a `AddressBookChangedEvent` when the Address Book data are changed, instead of asking the `Storage` to save the updates to the hard disk. - -The diagram below shows how the `EventsCenter` reacts to that event, which eventually results in the updates being saved to the hard disk and the status bar of the UI being updated to reflect the 'Last Updated' time. - -.Component interactions for `delete 1` command (part 2) -image::SDforDeletePersonEventHandling.png[width="800"] - -[NOTE] -Note how the event is propagated through the `EventsCenter` to the `Storage` and `UI` without `Model` having to be coupled to either of them. This is an example of how this Event Driven approach helps us reduce direct coupling between components. +image::LogicClassDiagram1.png[width="800"] The sections below give more details of each component. [[Design-Ui]] === UI component - .Structure of the UI Component -image::UiClassDiagram.png[width="800"] +image::UiClassDiagram_Piconso.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, +`HistoryListPanel`, `FilmReel`, `ImagePanel` etc. All these, including the `MainWindow`, +inherit from the abstract `UiPart` class. -The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] +The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that +are in the `src/main/resources/view` folder. For example, the layout of the link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] The `UI` component, * Executes user commands using the `Logic` component. -* Binds itself to some data in the `Model` so that the UI can auto-update when data in the `Model` change. +* Binds itself to some images in the `Model` so that the UI can auto-update when data in the `Model` change. * Responds to events raised from various parts of the App and updates the UI accordingly. [[Design-Logic]] === Logic component - -[[fig-LogicClassDiagram]] .Structure of the Logic Component -image::LogicClassDiagram.png[width="800"] +image::LogicClassDiagram1.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `PiconsoParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. -. The command execution can affect the `Model` (e.g. adding a person) and/or raise events. +. The command execution can affect the `Model` (e.g. converting an image) and/or raise events. . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. +Given below is the Sequence Diagram for interactions within the Logic component for the execute("cd") API call. -.Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +.Interactions inside the Logic Component for the `cd` command +image::LogicComponentClassDiagram.png[width="800"] [[Design-Model]] === Model component .Structure of the Model Component -image::ModelClassDiagram.png[width="800"] +image::ModelClassDiagram_Piconso.png[width="800"] *API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the `Canvas` for the current image. * does not depend on any of the other three components. -[NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + - + -image:ModelClassBetterOopDiagram.png[width="800"] - [[Design-Storage]] === Storage component @@ -233,123 +181,819 @@ image::StorageClassDiagram.png[width="800"] The `Storage` component, * can save `UserPref` objects in json format and read it back. -* can save the Address Book data in xml format and read it back. [[Design-Commons]] === Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `seedu.address.commons` package. == Implementation - This section describes some noteworthy details on how certain features are implemented. +// tag::cd[] +=== Change Directory (Cd) feature +The Cd feature is implemented to allow users to access images in the different directories within their home system. +This removes the restrictions of accessing only images from one particular folder. + +==== Current Implementation +The Cd mechanism is facilitated by the `ModelManager`. It contains the operations `Model#getCurrDirectory()` and +`Model#updateCurrDirectory(Path)`. + +The Model calls upon `UserPrefs` within the respective operations. The `UserPrefs` class contains the current directory +the user's in, stored internally as `currDirectory` and implements the following operations: + +* `UserPrefs#getCurrDirectory()` - Returns the user's current directory. +* `UserPrefs#updateUserPrefs(Path)` - Updates the user's current directory with the updated Path. + +Within the operation `UserPrefs#updateUserPrefs(Path)`, it also retrieves the list of images within the directory, +which are stored internally as `imageList`. This would facilitate the `open` feature in our application +(refer to 3.2). + +Additionally, to ease user's experience, similar to the actual usage of the cd command, this feature also uses the `tab` +function to auto-complete the directory name if it exists. + +[NOTE] +Pressing tab again will display the next directory with the given prefix. + +Given below is an example usage scenario and how the cd mechanism behaves at each step. + +Step 1. The user launches the application for the first time. The `UserPrefs` will be initialized with the `currDirectory` +as the user's home directory. + +Step 2. The user executes `cd Desktop` command to navigate into the Desktop directory. The cd command calls +`Model#getCurrDirectory()` and appends `Desktop` to the end of the current directory. It then checks if the new Path is +a directory and calls `Model#updateCurrDirectory(Path)` and updates the new Path in `UserPrefs` if the check returns true. + +The following sequence diagram shows how the cd command works: + +.Sequence Diagram for CdCommand +image::CdSequenceDiagram.png[width=800] + +[NOTE] +If the `newCurrDirectory` is not a directory, i.e. `isDirectory()` returns false, then there is no change in +`currDirectory` state in `UserPrefs`. If so, it will return a failure message to the user rather than attempting to update +`currDirectory`. + +==== Design Considerations +This section contains the considerations and alternatives we had when implementing the cd command. + +===== Aspect: How cd executes + +* *Alternative 1 (current choice)*: Retrieves and updates current directory in `UserPrefs`. +** Pros: Easy to implement and every command can access the current directory. +** Cons: Appends and checks if path exists after every cd command entered. + +* *Alternative 2* : Stores path that exists in a HashSet. +** Pros: Do not need to append and check, and just check if it exists in HashSet. +** Cons: Does not update existing path if user deletes a directory. +// end::cd[] + +// tag::open[] +=== Open feature +The Open Command allow users to open the images in a batch of 10 images within the directory for image-editing. +This command is further facilitated by the Next/Prev Command. + +==== Current Implementation +The implementation of the Open feature is largely similar to the `Cd Command`. It is facilitated by the `ModelManager` +and contains the following operations: + + * `Model#getDirectoryImageList()` - Retrieves the stored list of images in UserPrefs. + * `Model#updateCurrentOriginalImage(Image, Path)` - Updates the model with the current image opened. + +The `Model` calls upon `UserPrefs` to retrieve the `imageList` of the current +batch. The `UserPrefs` class implements the following operation: + +* `UserPrefs#getCurrImageListBatch()` - Returns the current batch of images. + +The `Model#updateCurrentOriginalImage(Image, Path)` operation stores the path of the opened image and the +`PreviewImage` instance of it as `currOriginalImage` and `currentPreviewImage` within the `ModelManager`. +Additionally, the operation also creates a canvas and a layer to facilitate the `transformation` feature. (refer to 3.5) + +Given below is an example usage scenario and how the open mechanism behaves at each step. + +Step 1. The user launches the application for the first time. The `UserPrefs` will be initialized with the `currDirectory` +as the user's home directory. + +Step 2. The user executes `cd Desktop` command to navigate into the Desktop directory. The cd command will initialise +the `imageList` with all the images within Desktop. + +image::SelectCommand1.png[width=800] + +Step 3. The user executes `open 1` command to open the first image in the first batch of 10 images. The open +command calls `Model#getDirectoryImageList()` to retrieve the first batch of images within Desktop. The first image is +then retrieved and displayed on the GUI. + +image::OpenCommand2.png[width=500] + +Step 4. The user then executes `open 5` command to open the fifth image in the batch of 10 images. The fifth image +is then retrieved similarly and displayed on the GUI. + +image::OpenCommand3.png[width=450] + +The following sequence diagram shows how the open command works: + +.Sequence Diagram for OpenCommand +image::OpenSequenceDiagram.png[width=800] + +==== Design Considerations +This section contains the considerations and alternatives we had when implementing the open command. + +===== Aspect: How open executes + +* *Alternative 1 (current choice)*: Open images within the BATCH_SIZE. +** Pros: Users work on a small size of images. +** Cons: Limited to the batch size. + +* *Alternative 2* : Open images within the `imageList` size. +** Pros: Easy to retrieve images anywhere in the list. +** Cons: Can be very messy if the `imageList` size is too large. +// end::open[] + +// tag::nextprev[] +=== Next/Prev feature + +`NextCommand` retrieves the next 10 images in the image list (image list refers to images in the current directory). + +On the contrary, the `PrevCommand` retrieves the previous 10 images in the image list. + +==== Current Implementation +The implementation of the Next/Previous feature is similar to the `CdCommand`. It is facilitated by the `ModelManager` +and contains the operations: `Model#updateImageListNextBatch()` and `Model#updateImageListPrevBatch()`. + +The `Model` calls upon `UserPrefs` which stores and facilitates the retrieval of the current +batch of images using the `currBatchPointer`. The `UserPrefs` class implements the following operation: + +* `UserPrefs#updateImageListNextBatch()` - Adds the `currBatchPointer` by 10. +* `UserPrefs#updateImageListPrevBatch()` - Minus the `currBatchPointer` by 10. + +Given below is an example usage scenario and how the next/previous mechanism behaves at each step. + +Step 1. The user launches the application for the first time. The `UserPrefs` will be initialized with the `currDirectory` +as the user's home directory. + +Step 2. The user executes `cd Desktop` command to navigate into the Desktop directory. The cd command will initialise +the `imageList` with all the images within Desktop. + +image::SelectCommand1.png[width=800] + +Step 3. The user executes `next` command to retrieve the next 10 images within Desktop. + +image::NextPrevCommand2.png[width=800] + +Step 4. The user executes `prev` command to retrieve the previous 10 images within Desktop. + +image::NextPrevCommand3.png[width=800] +// end::nextprev[] + +The following sequence diagram shows how the next command works: + +.Sequence Diagram for NextCommand +image::NextSequenceDiagram.png[width=800] + +The following sequence diagram shows how the prev command works: + +.Sequence Diagram for PrevCommand +image::PrevSequenceDiagram.png[width=800] + +// tag::nextprev[] +==== Design Considerations +This section contains the considerations and alternatives we had when implementing the next/prev command. + +===== Aspect: How next/previous executes +* *Alternative 1 (current choice)*: Keeps track of current batch with a pointer. +** Pros: Easy to access current batch images. +** Cons: Efficiency might be lower for directories with many images. + +* *Alternative 2* : Store image batches in array. +** Pros: Fast to access next/previous batches. +** Cons: Harder to handle changes in a batch (e.g. image got deleted) within the array. +// end::nextprev[] + // tag::undoredo[] === Undo/Redo feature ==== Current Implementation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. +The `undo/redo` works on the `currentLayer` the user is working on. Each `Layer` contains a `PreviewImage` which facilitates the undo/redo mechanism. +The mechanism works by caching the original image and transformed images in a temporary `cache` folder, and using `currentStatePointer` as a pointer together with `currentSize` as an indicator to manage the caching. +Undoing and redoing will shift the `currentStatePointer` accordingly while each transformation commits the image by writing to the `cache` folder (purging redundant images if needed). + +[NOTE] +To work with multiple layers, each `Layer` has a single `PreviewImage` which is initialized with a unique `LayerId`, so that the `PreviewImage` can cache its images safely without conflicting image names. + Additionally, it implements the following operations: -* `VersionedAddressBook#commit()` -- Saves the current address book state in its history. -* `VersionedAddressBook#undo()` -- Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` -- Restores a previously undone address book state from its history. +* `PreviewImage#commit()` - Writes the newly transformed `BufferedImage` into the `cache` folder (purge redundant images if needed). +* `PreviewImage#getCurrentPath()` - Returns the current state's `Path` in the `cache` folder for ImageMagick to use. +* `PreviewImage#undo()` - Shifts the `currentStatePointer` to the left, pointing to the previous state. +* `PreviewImage#redo()` - Shifts the `currentStatePointer` to the right, pointing to a previously undone state. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +These operations are exposed in the `Model` interface as `Model#updateCurrentPreviewImage()`, `Model#undoPreviewImage()` and `Model#redoPreviewImage()`. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +Step 1. The user opens an image with the `open` command. The `Canvas` is initialized with a new `Layer` which initializes its own `PreviewImage` with the opened image. The `currentStatePointer` is pointing to that state. -image::UndoRedoStartingStateListDiagram.png[width="800"] +[NOTE] +A `Canvas` can already be initialized, meaning this is an additional layer being added. The `Layer` and `PreviewImage` gets initialized the same way. -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +.After opening an image +image::undoRedo1.png[width="800"] -image::UndoRedoNewCommand1StateListDiagram.png[width="800"] +Step 2. The user executes a series of transformations. Each time, the newly transformed `BufferedImage` is stored by writing it to the `cache` folder. The `currentStatePointer` is also incremented. Eg. `hue`, `mirror`, `blur` -Step 3. The user executes `add n/David ...` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +[NOTE] +If a command fails its execution, it will not call `Model#updateCurrentPreviewImage()`, so nothing will be cached. + +.After executing transformations +image::undoRedo2.png[width="800"] + +Step 3. The user wants to undo the previous action by using the `undo` command. It will call `Model#undoPreviewImage()` which will shift the `currentStatePointer` once to the left, pointing it to the previous `PreviewImage` state. After which, that previously cached `BufferedImage` will be read and rendered to update the UI's preview image pane. -image::UndoRedoNewCommand2StateListDiagram.png[width="800"] [NOTE] -If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +If the `currentStatePointer` is at index 0, pointing to the initial state, then there are no previous states to restore. The `undo` command uses `Model#canUndoPreviewImage()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +.After exucuting undo +image::undoRedo3.png[width="800"] -image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] -[NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. +Step 4. The user executes another transformation, which calls `Model#updateCurrentPreviewImage`. Since the `currentStatePointer` is not pointing at the end state (`currentSize - 1`), and the states after the `currentStatePointer` will not make sense, all states after the pointer will be purged. + +.After comitting and purging redundant states +image::undoRedo4.png[width="800"] + +//Step 5. The user decides to undo several actions. The user uses the `undo` command followed by the number of actions to undo eg.`2`. The `currentStatePointer` derements accordingly. The model's `previewImage` is to the pointed state. +// +//image::undoRedo5.png[width="800"] The following sequence diagram shows how the undo operation works: -image::UndoRedoSequenceDiagram.png[width="800"] +.Sequence Diagram for Undo/RedoCommand +image::undoRedoSequenceDiagram.png[width="800"] -The `redo` command does the opposite -- it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +The redo command does the opposite — it calls `Model#redoPreviewImage()`, which shifts the currentStatePointer once to the right, pointing to the previously undone state. [NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +If the `currentStatePointer` is at index `currentSize - 1`, pointing to the `PreviewImage's` last state, then there are no undone states to restore. The `redo` command uses `Model#canRedoPreviewImage()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. + + +The following activity diagram summarizes what happens when a user executes a new transformation: + +.Activity Diagram for performing a new image transformation +image::undoRedoActivityDiagram.png[width="800"] + +The following activity diagram summarizes what happens when a user executes the undo command: + +.Activity Diagram for using `undoCommand` +image::undoRedoActivityDiagram2.png[width="800"] -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +The following activity diagram summarizes what happens when a user executes the redo command: -image::UndoRedoNewCommand3StateListDiagram.png[width="800"] +.Activity Diagram for using `redoCommand` +image::undoRedoActivityDiagram3.png[width="800"] -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. +==== Undo-all and Redo-all +The commands `undo-all` and `redo-all` follow the same implementation as undo and redo. They provide a convenient way to quickly undo and redo all transformations to the current layer's `PreviewImage` for the user. -image::UndoRedoNewCommand4StateListDiagram.png[width="800"] +* `undo-all` shifts the `PreviewImage's` pointer to 0, pointing at the original state. -The following activity diagram summarizes what happens when a user executes a new command: +* `redo-all` shifts the `PreviewImage's` pointer to `currentSize - 1`, the state with all the applied transformations. + +==== HistoryListPanel + +The HistoryListPanel provides a view for the user to see the history of transformations applied. +Whenever a transformation is done or when a undo/redo command is executed, the `Model#refreshHistoryList()` is executed which refreshes the `HistoryListPanel` with a `HistoryUpdateEvent` containing the current layer's `PreviewImage's` list of transformations. +A view of the panel is shown below: + +.Image of HistoryListPanel +image::historyPanel.png[width="200"] -image::UndoRedoActivityDiagram.png[width="650"] ==== Design Considerations ===== Aspect: How undo & redo executes -* **Alternative 1 (current choice):** Saves the entire address book. -** Pros: Easy to implement. -** Cons: May have performance issues in terms of memory usage. -* **Alternative 2:** Individual command knows how to undo/redo by itself. -** Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). -** Cons: We must ensure that the implementation of each individual command are correct. - -===== Aspect: Data structure to support the undo/redo commands - -* **Alternative 1 (current choice):** Use a list to store the history of address book states. -** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. -** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. -* **Alternative 2:** Use `HistoryManager` for undo/redo -** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. -** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. +* **Alternative 1 (current choice):** Saves each newly transformed image (including original). +** Pros: Fast execution of undo/redo as only reading the cached file is needed. Easy to implement. +** Cons: Uses the user’s storage space temporarily for caching. + +* **Alternative 2:** Save only the command, and reverse/reapply transformation for each undo/redo. +** Pros: Will not need to use user's data storage. +** Cons: Transformations on images take significantly more time as compared to reading and writing cache. Also, reversing of transformations are not possible for commands like `blur` and `colorspace`. // end::undoredo[] -// tag::dataencryption[] -=== [Proposed] Data Encryption +// tag::transformation[] + +=== Transformations and ImageMagick +==== Current Implementation + +The transformation mechanism is facilitated by https://www.imagemagick.org/script/index.php[`ImageMagick`], an external API Library that helps process the target image by the specified commands given. +The transformation feature mainly consists of: + +* `ImageMagicUtil`: A class which will check, create, and run the imageMagick executable file +* `applyCommand`: Applies the specified transformation to the image +* `createApplyCommand`: Allows users to define their own custom transformation set. + +The following describes main operations and processes contained in the classes above. + +==== ImageMagickUtil: + +This class is used to provide utility methods to facilitate interoperability with ImageMagick such as: + +* `copyOutside()` -- Copies the OS-specific payload for ImageMagick. +* `parseOperationArguments()` -- Parse the build in arguments and customised operations, return a list of String passed into process builder. +* `processImage()` -- Converts user input to a valid ImageMagick command. +* `runProcessBuilder()` -- Given a valid ImageMagick command, runs the command and retrieves the output. + +This class is separated from the model and acts as a Util for image editing, whenever editing the image, +a transformation will be passed to the processImage, and it will be parses basing on the command templates +stored in the app, then a list of arguments will be parsed from the transformation and a new process builder will be created +and call the ImageMagick executable file to handle the image processing. + +When starting the app, methods in ImageMagickUtil will be called to check the environment and prepare the ImageMagick package. + +.Activity Diagram for Transformation feature with ImageMagick +image::TransformationActivityDiagram.png[width="800"] + +===== Design Considerations: +As ImageMagick is an independent portable package and it differs from system to system, in order to make sure that +piconso will be able to work on multiple operating system, it needs to check the current system, choose the correct package +and prepare the suitable environment for using ImageMagick. + + +==== ApplyCommand: + +This command validates a transformation provided by the user and attempts to add it to the current `PreviewImage`++'s++ `TransformationSet`. + +Given below is an example usage scenario and how the command should behave at each step. + +Step 1. Upon first running the app, the corresponding zip package containing ImageMagick will be copied out of the jar and unzipped. A temporary folder for ImageMagick output is also created. + +Step 2. The user opens an image with the `open` command. The method `model.getPreviewImage()` will be called to read the appropriate `BufferedImage` from disk. + +Step 3. The user enters the command (e.g. `apply blur 0x8`). The entered command is parsed and the corresponding transformation is created. + +[NOTE] +If an invalid transformation is provided, a reminder of how to use the command will be given to the user and no ImageMagick command will be run. + +Step 4. Call `ImageMagick#processImage` which is a quasi-factory method that creates an instance of `ProcessBuilder` which contains the all the necessary information for ImageMagick to apply a transformation to the image, +executes the `ProcessBuilder` and reads the output back from the temporary folder. +[NOTE] +While normal `apply` commands will show a useful help message after a failure, an `apply raw` command cannot provide the same granularity and will return a generic error message. + +Step 5. If successful, the transformation that is just executed will be added to the `PreviewImage`++'s++ `TransformationSet`. + +Step 6. The command will then request the UI to update its components, namely the `HistoryListPanel` and `ImagePanel` if any transformation was applied. + +===== Design Considerations + + - As the ImageMagick executable file is only able to handle command line input and requires the path of the image to modify, + intermediate images have to be stored in a temp folder first. + + - For senior users, or users who are familiar with ImageMagick, `apply raw` will provide more flexibility and enable them to do further image editting. + +.Sequence Diagram for Apply Command +image::ApplyCommandSequenceDiagram.png[width="800"] + + +==== CreateApplyCommand: + +This command allows users to create a named set of commands and store in the disk. All transformation sets will first be validated by `ImageMagickUtil#parseCustomiseCommand`. + +Given below is an example usage scenario and how the transformation should behave at each step. + +Step 1. The user sequentially enters the transformations he wants to store, in the form of `create [command name] (command|argument)`. For example, +`create blurgray blur|0x8 colorspace|GRAY` will create a new command named 'blurgray'. + +Step 2. The specified transformations are parsed and checked by the templates stored in the app. +[NOTE] +If any of the specified transformations have an invalid name or argument, an error will be thrown. + +Step 3. The validated new command will be stored in the json for in the PiconsoCommands folder create when first time run the app. + +.Activity Diagram for CreateApply Command +image::CreateTransformationActivityDiagram.png[width="800"] + +==== Design Considerations + + - Combination of the transformations are commonly needed in our daily lives. When users design a serious of operations and + plan to apply them to multiple images, or store them for future use, this feature will make the image editing more efficient + and flexible, thus we implemented the create command. + +// end::transformation[] + +// tag::save[] + +=== Save feature +==== Current Implementation +The save command saves the current preview image to the same location it was opened in the local drive, +User enters the name of the image and the format of the name will be validated. + +* `isFormatValid()` is to validate the format of the filename user enters + +Given is an example of how to use the command + +Step 1. Parse the argument entered, if no argument, store to original image. + +Step 2. If there is an argument, parse the image name user enters, get the name and the format of the filename. + +Step 3. Check the format of the image as well as the filename entered. If the format is not supported or the filename is already in use, +a reminder will be shown to user and the image will not be saved. + +==== Design Considerations +- as there is already an option for user to save to original image, when user specifies a filename, in order to avoid +unintended overwritten of other images, using existing filename is forbidden. + +.Sequence Diagram for Save Command +image::SaveCommandSequenceDiagram.png[width="800"] + +// end::save[] +// tag::canvas[] + +== Implementation: Canvas and Layer features + +=== General overview +The `Canvas` and `Layer` classes serve as a layer of encapsulation for handling one or more instances of `PreviewImage`. +This is in line with good defensive coding practices and separation of concerns. + +=== Canvas (canvas) command +There are a set of overloaded command types inheriting from `CanvasCommand`, namely: + +* `bgcolor` - Sets the background colour of the canvas. +* `size` - Sets the height and the width of the canvas. +* `auto-resize` - Toggles the auto-resize property of the canvas. + +==== Current implementation +Canvas manages various properties made accessible through the `ModelManager` class. +Layer order is preserved as it important for image composition. + +Canvas implements the following accessors and utility functions: + +* `getLayers()` - Returns an ordered list of layers which is guaranteed to be neither `null` or empty. +* `getLayerNames()` - Returns a list of layer names in order. +* `addLayer(PreviewImage, String)` - Adds a given `PreviewImage` into the canvas on its own layer. +* `getCurrentLayer()` - Returns the current instance of the layer that is being operated on. +* `getCurrentLayerIndex()` - Returns the index of the current instance of the layer that is being operated on. +* `setCurrentLayer(Index)` - Sets the layer to be operated on. +* `removeLayer(Index)` - Removes a layer at a given index. The current layer and the last remaining layer in a canvas cannot be removed. + +All of these functions are exposed through the `Model` interface. +Implementations in `ModelManager` usually include a call to the corresponding accessor along with utility functions to manage the state of the UI. + +Beneath the hood, `Canvas` manipulation is powered by ImageMagick. +All of the properties represented in `Canvas` are transformed into their respective flags or arguments in ImageMagick. +For example, the `isCanvasAuto` property is transformed into the flag `-flatten` if false and `-layers merge` if true. + +The `processCanvas` is a factory method that handles this transformation, returning an instance of a `ProcessBuilder`. +The `ProcessBuilder` is then passed to `ImageMagickUtils#runProcessBuilder` which executes the process and stores the output in a BufferedImage. + +.Sequence Diagram for Canvas Command +image::CanvasSequenceDiagram.png[width="2000"] + +The above diagram illustrates the process of parsing and executing `canvas` commands. + +. The user executes any command beginning with `canvas`. + +. The command is first parsed by `PiconsoParser` which picks up the `canvas` keyword and passes any remaining arguments to `CanvasCommandParser`. + +. `CanvasCommandParser` determines the appropriate sub-command being executed and performs the requested operation on the canvas. + +==== Design considerations + +**Manipulation of the `ProcessBuilder`** + +[.underline]#Alternatives# + +Storing arguments directly in a `List` where T is a custom type that implements `Comparable` to ensure that arguments are in the correct order. A helper function will then map the `T` to a `String` and the resultant `List` will be used to construct the necessary `ProcessBuilder`. + +[.underline]#Evaluation# + +While insertion of new layers and properties will be extremely easy, modifying or removal of existing properties will involve searching through the entire list. As a result, this method is extremely hard to grok and performs poorly upon update or deletion of existing properties. +// end::canvas[] + +// tag::layer[] +=== Layer (layer) commands + +The `layer` command follows a similar pattern as the `canvas` command. +The following sub-commands inherit from `LayerCommand`: + +* `add` - Adds a layer from the `Canvas` and generates a layer name. +* `delete` - Removes a layer from the `Canvas`. The current layer and the last remaining layer cannot be removed. +* `select` - Selects a layer to work on. +* `swap` - Swaps the order of two distinct layers. + +==== Current implementation + +`Layer` implements a few key accessors and utility functions. Some of them include: + +* `addTranformation(Transformation)` - Adds a given transformation into its `PreviewImage`++'s++ `TransformationSet`. +* `getName()` - Gets the name of the layer. +* `setHeight(int)` - Sets the height of the layer. +* `setWidth(int)` - Sets the width of the layer. +* `setPosition(int, int)` - Sets the x and y coordinates of the layer. + +The following image illustrates the coordinate system adopted in Piconso. + +image::coords.png[width="300"] + +The default anchor point is the top-left corner, this means that a `layer position -10x-10` command will set that layer's top left corner at `(-10, -10)`. +It is possible to have negative co-ordinates although clipping will occur unless the canvas is set to auto-resize. + +.Sequence Diagram for Layer Command +image::LayerSequenceDiagram.png[width="2000"] + +The above diagram illustrates the process of parsing and executing of `canvas` commands. + +. The user executes any command beginning with `layer`. + +. The command is first parsed by `PiconsoParser` which picks up the `layer` keyword and passes any remaining arguments to `LayerCommandParser`. + +. `LayerCommandParser` determines the appropriate sub-command being executed and performs the requested operation on the canvas and current layer. + +==== Design Considerations + +**Further manipulation of the `ProcessBuilder`** + +_Alternatives_ + +It is actually possible to nest `ImageMagick` commands which means that it is possible to keep separate `List`++s++ and conjugate them when the canvas needs to be rendered. +The resultant ImageMagick command will take the form : + + magick [overall canvas flags] {[canvas flag] (individual layer flags)} [overall canvas flags] + +Where blocks enclosed by `{ }` need to be repeated per layer. + +_Evaluation_ + +This solution is the most straight-forward and results in no intermediate files which is usually desirable. + +However, the resultant ImageMagick command will short circuit and cause the entire expression to fail if any of flags are incompatible or incorrect. Caching is also impossible, causing the whole canvas and all of its layers to be composed again from scratch. +This results in a poor user experience and hence we have decided against it. + +=== Canvas and Layers in action + +As the `canvas` and `layer` commands compliment each other, let's walk through a typical user's session in Piconso from start to finish. + +. The user executes a valid `open` command: +A `Canvas` is constructed holding exactly one `Layer` +with the default height and width being that of the image selected. + +. The user adds a new layer to the canvas: +`Canvas#addLayer` is executed and the helper functions in `ModelManager` refreshes the UI. + +. The user swaps the order of the two layers to such that the original image is back on top: +`Canvas#swapLayer` swaps the two entries in the list of layers. + +. While still on the original layer, the user moves it to the top left: `Layer#setPosition` moves the first layer out of the way. + +. The user decides to work on the second layer and enters `layer select 2`: +`Canvas#setCurrentLayer` changes the current layer to the given index and internally keeps track of the index as well. + +. A transformation is applied by the user to the current layer: `Canvas#addTransformation` appends the new Transformation to the current Layer. +Note that no image processing occurs until a new render is requested. + +. After a `canvas size 80x60`command: The new `Canvas#setSize` is applied upon the next render and it crops the image to a fraction of what it used to be. +Remember that resizing and scaling is accomplished with the `apply resize` command. + +. Realising his mistake, the user sets the canvas to fit his all of his layers: `Canvas#setCanvasAuto` toggles a boolean. + +. The user fills in the default transparent background with a `canvas bgcolor ++#++707070` : a hex color code is accepted by `Canvas#setBackgroundColor`. + +.Results of various commands in sequence. The black bounding rectangle indicates the canvas size. +image::CanvasLayersDemo.png[width="2000"] + +// end::layer[] + +// tag::google[] + +== Implementation: Google features +=== Overall Introduction +The Google commands allow for access to https://developers.google.com/photos/library/guides/get-started-java[Google Photos] through a logged-in instance of the user, and are held up by two main components. + +* `PhotosLibraryClientFactory` - Initiates and carries out the login process. Produces a `PhotoHandler` instance, which handles matters related to Google commands. +* `PhotoHandler` - Mainly consists of a `PhotoLibraryClient` instance, the user's logged in state, and 3 maps for storing images, albums, and specific images from an album. Performs all explicit calls to Google Photos through the `PhotosLibraryClient` instance and parses results. +** The `PhotoHandler` instance is later accessed through `Model#getPhotoHandler()` and `Model#setPhotoHandler()``. + +There are 6 main google-related commands. The first two commands login/logout a user, and the latter 4 are mostly overloaded command types with `GoogleCommand` as the abstract parent class. + +* `LoginCommand` - Logs in user to their Google Account. +* `LogoutCommand` - Logs a user out of their Google Account. +* `GoogleLsCommand` - Lists files in Google Photos. +* `GoogleRefreshCommand` - Refreshes the displayed list from Google Photos. +* `GoogleDlCommand` - Downloads the specified image(s) from Google Photos to the user's currently opened local directory. +* `GoogleUploadCommand` - Uploads the specified image(s) from the user's currently opened local directory to Google Photos. + +We have connected and generated `client_credentials.json` via our own Google Account to enable usage of Google Photos API in Piconso. It is recommended that you https://developers.google.com/photos/library/guides/get-started-java[configure] Piconso to use Google Photos Library API with your own account rather than the provided. + + +If you are not familiar with how Google Photos works, it would be advisable to first try out Google Photos as a consumer before proceeding. + +=== Login (login) Command -_{Explain here how the data encryption feature will be implemented}_ +==== Current Implementation +The Login command currently authenticates a user via Google OAuth. To learn more about the implementation of OAuth methods, you may refer to https://developers.google.com/identity/protocols/OAuth2#installed[O-Auth Explanation] and https://developers.google.com/api-client-library/java/google-api-java-client/oauth2#installed_applications[Google API examples]. As the workings of Google OAuth are rather complicated, it is suggested that you first go through the examples/documentations in those links. + + +The following static methods of `PhotosLibraryClientFactory` will be involved: + +* `createClient()` - Creates a PhotoLibrary instance, contains calls to Google OAuth related methods. +* `createPhotosLibraryClient(Credentials)` - Creates an instance of Google's PhotosLibraryClient using the provided credentials. +* `getUserEmail(Credential)` - Retrieves the user's email from Google+. +* `loginUserIfPossible()` - Logs in a user if a valid credential file can be found during start up. +* `logoutUserIfPossible()` - Logs a user out if a credential file can be found. +* `checkUserLogin()` - Checks if any users are currently logged in. + +Below are some examples on how the login command will work. + +[NOTE] +If connection to the internet is lost at any point during authentication with Google's server, login will fail and an error message will be sent to user as feedback. + +==== Scenario 1: Explicit `login` command executed, user not logged in yet. + +. The user executes a `login` command. + +. The login command calls `Model#getPhotoHandler()` and checks if a PhotoHandler instance already exists. If not, it calls static method `PhotoLibraryClientFactory#createClient()` to set up the requirements for log in and redirects the user to the browser. + +This occurs asynchronously, and a user can close the redirect window and proceed as usual with the application if they have changed their mind. + +. User logs in through redirect window and a refresh token is returned and stored. PhotoLibraryClientFactory class then calls `PhotoLibraryClientFactory#createPhotosLibraryClient()` and `PhotoLibraryClientFactory#getUserEmail()` to instantiate a `PhotoHandler` instance + +. The `PhotoHandler` instance is set by model as `Model#photoLibrary`, and confirmation of login is sent to user. + +The following sequence diagram illustrates how the above steps work: + +.Sequence Diagram for LoginCommand +image::LoginSequenceDiagram.png[width="1000"] + +==== Scenario 2: Implicit login, where Piconso auto logs in user upon re-launch + +. Upon Piconso start up, `PhotoLibraryClientFactory#loginUserIfPossible()` is run by `ModelManager` + + +. The method checks for stored credentials (refresh token), and logs in user via `PhotoLibraryClientFactory#createClient()` if possible, else the log in process is skipped. + +In both scenarios, whenever a valid refresh token is found stored the user is logged in without having to face browser re-direct again. At no point in time will we be storing a user's actual credentials, but only a refresh token that allows us to keep a user logged in, actual credentials are handled by Google OAuth + + +==== Design Considerations +*Asynchronous Login*: By default, as long as a user has not logged in after `login` command is launched, Piconso freezes and waits for user input. + +** _Current Solution_: An asynchronous approach has been implemented instead such that Piconso continues working as usual even if the user has not logged in. Once a user is logged in, Piconso will simply show feedback. +** _Cons_: Google allows max 3 login requests to be made in an interval, hence the `login` command will stop redirecting after 3 consecutive logins made in a short period of time. While it is unlikely this will pose a problem for users, it would be advisable to display the redirect URL in the Piconso CLI during future development. + +==== Logout (logout) Command +The logout command deletes the stored credential file (and any other login-related files) if it exists, and does nothing if it does not. Upon deleting the file, the user will no longer have a refresh token to stay logged in, and thus is effectively logged out. + +=== GoogleLsCommand (ls) + +[NOTE] +For all Google-related Commands (excluding login and logout), their command word is appended with a g (i.e `g ls`). Thus all children of `GoogleCommand` are first passed through a `GoogleCommandParser` to determine its type. + +==== Current Implementation +The `GoogleLsCommand` allows users to browse through their stored images on Google Photos. Currently, it is overloaded with three types of commands the user can type + +`g ls` -> Lists all photos in user's Google Photos, takes a longer amount of time depending on the number of images stored. + +`g ls /a` -> Lists all albums in user's Google Photos. + +`g ls ` -> Lists all photos in specified album from Google Photos. + +As such, parsing will be done twice. Once by `GoogleCommandParser`, and another within `GoogleLsCommand` itself. + +The following methods and components of `PhotoHandler` will be involved: -// end::dataencryption[] +* `imageMap` - Map that contains the list of images retrieved from Google API call. Uses image names as keys. +* `albumMap` - Map that contains the list of albums retrieved from Google API call. Uses album names as keys. +* `specificAlbumMap` - Map that contains the list of images from a specific album retrieved from Google API call. Uses image names as keys. -=== Logging +* `getUniqueName(Map, String, String)` - Checks for existence of a name in the map. If it exists, append a sutiable index at the end of the name. +* `refreshLists()` - Re-retrieves all images and albums from Google Photos and updates `imageMap` and `albumMap` +* `returnAllAlbumsList()` - Calls `retrieveAllAlbumsFromGoogle()` and returns a `List` of Album names for easy display +* `retrieveAllAlbumsFromGoogle()` - Makes a Google API call and retrieves all albums stored in Google Photos, storing it in `albumMap`. + ** Similar methods for other variants exist, but only methods used for the example below will be listed. -We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. +==== Example: First call for `g ls /a`, where user wants to list all albums in Google Photos. -* The logging level can be controlled using the `logLevel` setting in the configuration file (See <>) -* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level -* Currently log messages are output through: `Console` and to a `.log` file. +. The user executes a `g ls /a` command. -*Logging Levels* +. The command goes through parsing, firstly by `GoogleCommandParser` and secondly filtered by GoogleLsCommand. It is determined to be for listing albums, and `model.getPhotoHandler().returnAllAlbumsList()` is called. -* `SEVERE` : Critical problem detected which may possibly cause the termination of the application -* `WARNING` : Can continue, but with caution -* `INFO` : Information showing the noteworthy actions by the App -* `FINE` : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size +. Within that method, it makes a request to Google Photos, retrieves a `List`, and stores them in a `Map` within the PhotoHandler instance, with each key being the album name, any duplicate names are renamed. + +. The entire list is converted into a String, and returned to the `CommandBox` UI as feedback to the user + +The following activity diagram illustrates how the above steps work: + +.Activity Diagram for GoogleLsCommand +image::GoogleLsActivityDiagram.png[width="1000"] + +The process is similar for the other 2 variants, except images or images from a specific album are retrieved instead. All retrieved results are always stored in their respective maps. + +==== Design Considerations +*Performance Issues*: The larger the amount of pictures stored in Google Photos, the longer amount of time a `g ls` command takes. + +* _Alternative 1_: Re-retrieve the list of photos/albums upon every `ls` call +** Pro: Displayed list of images/albums is always up-to-date. +** Con: Each Google Photos API call takes a notable amount of time to execute. Constantly re-retrieving the list of images/albums causes multiples API calls and incurs heavy waiting time for users. + +* _Alternative 2 (Current)_: Adding a refresh command. As it is not likely that the set of photos in Google Photos will constantly change, the list of images/albums is retrieved and stored upon first `g ls` call and is only refreshed upon `g refresh`. +** Pro: Shortens the waiting time for user on every `g ls` call +** Con: More work on user's behalf to remember to refresh. + +*Duplicated Naming*: Google Photos allows for multiples images and albums to be stored with the same name, making it difficult to list item names as names might overlap. + +** _Alternative 1 (Current)_: Upon a retrieval of images/albums from Google, image/album names are checked for duplicates. Every duplicate will have a suitable index appended at the end of its original name to ensures that unique naming occurs. +*** Con: If there are multiple images with the same name, the process of any `g ls` or `g refresh` will take a longer time to run. + +** _Alternative 2_: Instead of storing the name of the image/album as the key inside the Map, its unique ID (that is retrieved together with the files from Google) can be used instead. +*** Con: The ID too complicated for a user to input and remember. + +*Album Traversal: How to display and use Albums from Google* + +* _Alternative 1_: Treat the album as a category, thus `g ls ` acts as a filter that filters photos by category. +** Pro: Easy for users to understand and remember. +** Con: The command to download from an album needs to be extended such that users need to specify which album the image to download is in i.e `g dl /a /i` + +* _Alternative 2_: Handle the concept of albums like directories, such that a user can cd in and out of an album. +** Con: Confusing for users to concurrently navigate and differentiate both google and local directories. + +==== GoogleRefreshCommand (refresh) +The refresh command re-retrieves and stores the list of images/albums from Google Photos into respective maps in `Model#getPhotoHandler()`. + +=== GoogleDlCommand (dl) + +==== Current Implementation +The GoogleDlCommand allows users to download their stored images on Google Photos. Currently, it is overloaded with three types of commands the user can type + +`g dl /i`: Downloads specified image from Google Photos + +`g dl /a`: Downloads all images from specified album in Google Photos, takes a longer amount of time depending on the number of images stored in the album. + +`g dl /a /i`: Downloads a specific photo from a specific album in Google Photos. + +As such, parsing will be done twice. Once by GoogleCommandParser, and another within GoogleDlCommand itself. + +The following components of `PhotoHandler` will be involved: + +* `downloadImage(String, String, String)` - Ensures the request is valid and calls saveImageInDir()` if true. +* `saveImageInDir(MediaItem, String)` - Saves the image using the baseURL of the `MediaItem` + ** Similar methods for other variants exist, but only methods used for the example below will be listed. + +==== Example: g dl /i` + +[NOTE] +For all upload/download related commands, the amount of time taken to process the task varies with the number of images to upload/download. + +. The user executes a `g dl /i` command. + +. The command goes through parsing, firstly by `GoogleCommandParser` and secondly filtered by GoogleDlCommand. It is determined to be for downloading one image, and `model.getPhotoHandler().downloadImage()` is called. + +. Within that method, it makes a request to Google Photos with the image's respective ID, and successfully downloads the image into the user's local directory. +[WARNING] +If the local directory contains any image with the same name as the one to download, the image in the local directory will be replaced. -[[Implementation-Configuration]] -=== Configuration +. Feedback of success is sent to the user -Certain properties of the application can be controlled (e.g App name, logging level) through the configuration file (default: `config.json`). +.Sequence Diagram for GoogleDlCommand +image::GoogleDlSequenceDiagram.png[width="800"] + +The process is similar for the other 2 variants, except images or images from a specific album are downloaded instead. + +==== Design Considerations +*Location of downloaded images*: + +* _Alternative 1 (Current)_: Download the image into user's currently opened local directory. +** Pro: User is able to easily find the new downloaded photo +** Con: Images with the same name WILL be replaced and it will be messy when multiple photos are downloaded + +* _Alternative 2 (Recommended for future)_: Images are downloaded into a specific directory which can be navigated to and fro via quick commands (i.e `jump to PicDir`, `jump back PicDir`) +** Pro: Easier for user to retrieve and view downloaded photos. Photos with the same name can also be safely replaced as they are likely earlier versions downloaded from Google Photos. + +=== GoogleUploadCommand (ul) + +==== Current Implementation +The GoogleUploadCommand allows users to upload their images onto Google Photos. Currently, it is overloaded with two types of commands the user can type + +`g ul `: Uploads specified image from currently opened directory to Google Photos + +`g ul all`: Uploads all images in currently open local directory. + +As such, parsing will be done twice. Once by GoogleCommandParser, and another within GoogleUploadCommand itself. + +As the implementation of GoogleUploadCommand is mostly dependent on understanding of the upload aspects of Google Photos API, an example will not be given. Please refer to https://developers.google.com/photos/library/guides/upload-media[this link]. + +The following components of `PhotoHandler` will be involved: + +* `uploadImage(String, String)` - Uploads the image from the current opened local directory. +* `retrievePiconsoAlbum()` - Retrieves the ID of the Piconso Album in Google Photos. If it does not exist, command creates one. +* `uploadMediaItemsToGoogle(List)` - Highly Google Photos API based, returns a `NewMediaItem` +* `generateNewMediaImage(String, String)` - Highly Google Photos API based, carries out actual upload to Google Photos with NewMediaItem. + ** Similar methods for other variants exist, but only methods used for the example below will be listed. + +==== Design Considerations +*Google Photos does not allow duplicate photos*: Illustrated in the diagram below + +* No renaming of photos after upload allowed. +* If an image is uploaded as "a.png", but exists with the exact same bytes as "b.png" on Google Photos, the image will replace the old image as b.png instead. +* If two images with the same bytes are sent for upload, only the first will be uploaded. + +.Illustrates when how Google Photos handles images + +image::uploadExplanation.png[width="800"] + +Currently the only measure taken has been to state this to the user. As this issue was only discovered in the later stages of development, it does not have a workaround yet. Though a lot of refactoring will be required, switching from Google Photos to Google Drive might be a fix for the problem. +// end::google[] == Documentation @@ -537,392 +1181,173 @@ A project often depends on third-party libraries. For example, Address Book depe a. Include those libraries in the repo (this bloats the repo size) + b. Require developers to download those libraries manually (this creates extra work for developers) -[[GetStartedProgramming]] [appendix] -== Suggested Programming Tasks to Get Started +== Product Scope -Suggested path for new programmers: +*Target user profile*: -1. First, add small local-impact (i.e. the impact of the change does not go beyond the component) enhancements to one component at a time. Some suggestions are given in <>. +* needs a quick and easy way to edit images +* has a lot of images to edit +* appreciates the power that traditional editing software provides +* familiar with the command line +* prefers typing over mouse input +* is reasonably comfortable using CLI app -2. Next, add a feature that touches multiple components to learn how to implement an end-to-end feature across all components. <> explains how to go about adding such a feature. +*Value proposition*: view, preview and edit images quickly in a streamlined, modular and repeatable process -[[GetStartedProgramming-EachComponent]] -=== Improving each component +[appendix] +== User Stories -Each individual exercise in this section is component-based (i.e. you would not need to modify the other components to get it to work). +Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` -[discrete] -==== `Logic` component +[width="59%",cols="22%,<23%,<25%,<30%",options="header",] +|======================================================================= +|Priority |As a ... |I want to ... |So that I can... +|`* * *` |photographer |edit an image |enhance an image -*Scenario:* You are in charge of `logic`. During dog-fooding, your team realize that it is troublesome for the user to type the whole command in order to execute a command. Your team devise some strategies to help cut down the amount of typing necessary, and one of the suggestions was to implement aliases for the command words. Your job is to implement such aliases. +|`* * *` |photographer |define my own set of transformations |avoid repetitive typing of commands -[TIP] -Do take a look at <> before attempting to modify the `Logic` component. +|`* * *` |photographer |see the preview of the transformations before committing to disk |explore the effects of transformations -. Add a shorthand equivalent alias for each of the individual commands. For example, besides typing `clear`, the user can also type `c` to remove all persons in the list. -+ -**** -* Hints -** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). -** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. -* Solution -** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. -** Add new tests for each of the aliases that you have added. -** Update the user guide to document the new aliases. -** See this https://github.com/se-edu/addressbook-level4/pull/785[PR] for the full solution. -**** +|`* *` |photographer |be able to upload my photos to the cloud (Google Photos) |safely store and share my photos across devices -[discrete] -==== `Model` component +|`*` |web developer or photographer |add captions or watermarks to images |to protect my intellectual property -*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. +|`*` |web developer |quickly recolor icons |easily use them in re-skinning my application -[TIP] -Do take a look at <> before attempting to modify the `Model` component. +|======================================================================= -. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the address book. -+ -**** -* Hints -** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] API need to be updated. -** Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags? -** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. -* Solution -** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`]. Loop through each person, and remove the `tag` from each person. -** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `AddressBook#removeTag(Tag)`. -** Add new tests for each of the new public methods that you have added. -** See this https://github.com/se-edu/addressbook-level4/pull/790[PR] for the full solution. -**** -[discrete] -==== `Ui` component +[appendix] +== Use Cases -*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how the users use your address book application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last person in the list. Your job is to implement improvements to the UI to solve all these problems. +(For all use cases below, the *System* is `Piconso` and the *Actor* is the `user`, unless specified otherwise) -[TIP] -Do take a look at <> before attempting to modify the `UI` component. +[discrete] +=== Use case: Editing an image -. Use different colors for different tags inside person cards. For example, `friends` tags can be all in brown, and `colleagues` tags can be all in yellow. -+ -**Before** -+ -image::getting-started-ui-tag-before.png[width="300"] -+ -**After** -+ -image::getting-started-ui-tag-after.png[width="300"] -+ -**** -* Hints -** The tag labels are created inside link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[the `PersonCard` constructor] (`new Label(tag.tagName)`). https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Label.html[JavaFX's `Label` class] allows you to modify the style of each Label, such as changing its color. -** Use the .css attribute `-fx-background-color` to add a color. -** You may wish to modify link:{repoURL}/src/main/resources/view/DarkTheme.css[`DarkTheme.css`] to include some pre-defined colors using css, especially if you have experience with web-based css. -* Solution -** You can modify the existing test methods for `PersonCard` 's to include testing the tag's color as well. -** See this https://github.com/se-edu/addressbook-level4/pull/798[PR] for the full solution. -*** The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storage, so that tags retain their colors even if the hash code algorithm changes. -**** - -. Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). -+ -**Before** -+ -image::getting-started-ui-result-before.png[width="200"] -+ -**After** -+ -image::getting-started-ui-result-after.png[width="200"] -+ -**** -* Hints -** link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] is raised by link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] which also knows whether the result is a success or failure, and is caught by link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] which is where we want to change the style to. -** Refer to link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] for an example on how to display an error. -* Solution -** Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] 's constructor so that users of the event can indicate whether an error has occurred. -** Modify link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)`] to react to this event appropriately. -** You can write two different kinds of tests to ensure that the functionality works: -*** The unit tests for `ResultDisplay` can be modified to include verification of the color. -*** The system tests link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest#assertCommandBoxShowsDefaultStyle() and AddressBookSystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. -** See this https://github.com/se-edu/addressbook-level4/pull/799[PR] for the full solution. -*** Do read the commits one at a time if you feel overwhelmed. -**** - -. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the address book. +*MSS* + +1. User opens an image +2. User uses the CLI to describe a set of transformations to the image +3. Piconso shows a preview of the outcome +4. User can save the outcome/transformation to disk + -**Before** +Use case ends. + +*Extensions* + +[none] +* 1a. The given image cannot be opened. + -image::getting-started-ui-status-before.png[width="500"] +[none] +** 1a1. Piconso shows an error message. + -**After** +Use case resumes at step 1. + +* 2a. User is unsatisfied with the last transformation. + -image::getting-started-ui-status-after.png[width="500"] +[none] +** 2a1. User can undo the last transformation. + -**** -* Hints -** link:{repoURL}/src/main/resources/view/StatusBarFooter.fxml[`StatusBarFooter.fxml`] will need a new `StatusBar`. Be sure to set the `GridPane.columnIndex` properly for each `StatusBar` to avoid misalignment! -** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the address book is updated. -* Solution -** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. -** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)`] to update the number of persons whenever there are new changes to the addressbook. -** For tests, modify link:{repoURL}/src/test/java/guitests/guihandles/StatusBarFooterHandle.java[`StatusBarFooterHandle`] by adding a state-saving functionality for the total number of people status, just like what we did for save location and sync status. -** For system tests, modify link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest`] to also verify the new total number of persons status bar. -** See this https://github.com/se-edu/addressbook-level4/pull/803[PR] for the full solution. -**** +Use case resumes at step 2. [discrete] -==== `Storage` component - -*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storage. +=== Use case: Define a set of transformations -[TIP] -Do take a look at <> before attempting to modify the `Storage` component. +*MSS* -. Add a new method `backupAddressBook(ReadOnlyAddressBook)`, so that the address book can be saved in a fixed temporary location. +1. User edits an image (from use case Editing an image) +2. Piconso displays transformations done on the left side pane +3. User enters command to save the set of transformations +4. Piconso requests for a name for the set +5. User enters a name +6. Piconso saves the set + -**** -* Hint -** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. -** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/XmlAddressBookStorage.java[`XmlAddressBookStorage`] class. -* Solution -** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. -**** - -[[GetStartedProgramming-RemarkCommand]] -=== Creating a new command: `remark` - -By creating this command, you will get a chance to learn how to implement a feature end-to-end, touching all major components of the app. - -*Scenario:* You are a software maintainer for `addressbook`, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible `remark` field for each contact, rather than relying on tags alone. After designing the specification for the `remark` command, you are convinced that this feature is worth implementing. Your job is to implement the `remark` command. - -==== Description -Edits the remark for a person specified in the `INDEX`. + -Format: `remark INDEX r/[REMARK]` - -Examples: - -* `remark 1 r/Likes to drink coffee.` + -Edits the remark for the first person to `Likes to drink coffee.` -* `remark 1 r/` + -Removes the remark for the first person. - -==== Step-by-step Instructions - -===== [Step 1] Logic: Teach the app to accept 'remark' which does nothing -Let's start by teaching the application how to parse a `remark` command. We will add the logic of `remark` later. - -**Main:** - -. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. - -**Tests:** - -. Add `RemarkCommandTest` that tests that `execute()` throws an Exception. -. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. - -===== [Step 2] Logic: Teach the app to accept 'remark' arguments -Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` - -**Main:** - -. Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. -. Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. - -**Tests:** - -. Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. -. Add `RemarkCommandParserTest` that tests different boundary values -for `RemarkCommandParser`. -. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. - -===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` -Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. - -**Main:** - -. Add a `Label` with any random text inside link:{repoURL}/src/main/resources/view/PersonListCard.fxml[`PersonListCard.fxml`]. -. Add FXML annotation in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] to tie the variable to the actual label. - -**Tests:** - -. Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. - -===== [Step 4] Model: Add `Remark` class -We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. - -**Main:** - -. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/person/Address.java[`Address`], remove the regex and change the names accordingly). -. Modify `RemarkCommand` to now take in a `Remark` instead of a `String`. - -**Tests:** - -. Add test for `Remark`, to test the `Remark#equals()` method. - -===== [Step 5] Model: Modify `Person` to support a `Remark` field -Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. - -**Main:** - -. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. -. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). -. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `addressBook.xml` so that the application will load the sample data when you launch it.) - -===== [Step 6] Storage: Add `Remark` field to `XmlAdaptedPerson` class -We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/XmlAdaptedPerson.java[`XmlAdaptedPerson`] to include a `Remark` field so that it will be saved. - -**Main:** - -. Add a new Xml field for `Remark`. - -**Tests:** - -. Fix `invalidAndValidPersonAddressBook.xml`, `typicalPersonsAddressBook.xml`, `validAddressBook.xml` etc., such that the XML tests will not fail due to a missing `` element. - -===== [Step 6b] Test: Add withRemark() for `PersonBuilder` -Since `Person` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. - -**Tests:** - -. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`]. This method will create a new `Remark` for the person that it is currently building. -. Try and use the method on any sample `Person` in link:{repoURL}/src/test/java/seedu/address/testutil/TypicalPersons.java[`TypicalPersons`]. - -===== [Step 7] Ui: Connect `Remark` field to `PersonCard` -Our remark label in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. - -**Main:** - -. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Person` 's remark. - -**Tests:** - -. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the now-functioning remark label. - -===== [Step 8] Logic: Implement `RemarkCommand#execute()` logic -We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logic for our `remark` command. - -**Main:** - -. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. - -**Tests:** - -. Update `RemarkCommandTest` to test that the `execute()` logic works. - -==== Full Solution - -See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. - -[appendix] -== Product Scope - -*Target user profile*: - -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing over mouse input -* is reasonably comfortable using CLI apps - -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app - -[appendix] -== User Stories - -Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` - -[width="59%",cols="22%,<23%,<25%,<30%",options="header",] -|======================================================================= -|Priority |As a ... |I want to ... |So that I can... -|`* * *` |new user |see usage instructions |refer to instructions when I forget how to use the App - -|`* * *` |user |add a new person | - -|`* * *` |user |delete a person |remove entries that I no longer need - -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +Use case ends. -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +*Extensions* -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +[none] +* 5a. The input name is already used. ++ +[none] +** 5a1. Piconso asks if user wants to overwrite to previously stored set. ++ +Use case resumes at step 4. -_{More to be added}_ +[discrete] +=== Use case: Add caption or watermark to image -[appendix] -== Use Cases +*MSS* -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +1. User opens an image +2. Piconso displays the image in the preview pane +3. User adds caption or watermark image through input +4. Piconso displays the caption/watermark in the preview pane atop the first layer. ++ +Use case ends. [discrete] -=== Use case: Delete person +=== Use case: Upload to Google Photos *MSS* -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User request to login to Google Photos +2. Piconso redirects the user to a login browser page +3. User logs in through browser page +4. Piconso shows feedback message for success +5. User uploads a photo +6. Piconso contacts Google Photos and uploads the photo +7. Piconso shows feedback for successful upload + Use case ends. *Extensions* [none] -* 2a. The list is empty. +* 3a. User does not login through browser window. + -Use case ends. +[none] +** 3a1. User can resume on any use case not involving Google interaction. -* 3a. The given index is invalid. +[none] +* 3b. Piconso loses internet connection. + [none] -** 3a1. AddressBook shows an error message. +** 3b1. Piconso informs user of disconnection and suggests for them to try again. + -Use case resumes at step 2. +Use case resumes at step 1. -_{More to be added}_ +[none] +* 5a. An error occurs while uploading. ++ +[none] +** 5a1. Piconso informs user of upload failure and suggests for them to try again. ++ +Use case resumes at step 5. [appendix] -== Non Functional Requirements -. Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +== Non Functional Requirements +. Should work on any mainstream OS as long as it has Java 9 or higher installed. . A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. - -_{More to be added}_ +. Should have easy to understand and remember user commands. +. Allows users to navigate directories efficiently. [appendix] == Glossary -[[mainstream-os]] Mainstream OS:: -Windows, Linux, Unix, OS-X +[[batch]] Batch:: +Images that are currently being viewed. For example, if there are 16 images in the current directory, the default "batch" viewed would be the first 10 photos. Upon a `next` command, the "batch" switches to the next 10. -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others - -[appendix] -== Product Survey - -*Product Name* - -Author: ... - -Pros: - -* ... -* ... - -Cons: - -* ... -* ... +[[transformation]] Transformation:: +Action that is performed on an image [appendix] == Instructions for Manual Testing -Given below are instructions to test the app manually. +Given below are instructions to test Piconso manually. [NOTE] These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing. @@ -941,26 +1366,257 @@ These instructions only provide a starting point for testers to work on; testers .. Re-launch the app by double-clicking the jar file. + Expected: The most recent window size and location is retained. -_{ more test cases ... }_ +=== Changing a directory + +. Traversing between directories + +.. Prerequisites: Users is aware of the current directory they are in from the status bar footer. +.. Test case: `cd Desktop` + + Expected: User is now in Desktop. The full path of Desktop is shown in the status message. The current directory in the status bar is updated. +.. Test case: `cd ..` + + Expected: User is now in the previous directory. The full path is shown in the status message. The current directory in the status bar is updated. +.. Test case (only for Windows): `cd C://Users` + + Expected: User is now in `C:/Users`. The full path is shown in the status message. The current directory in the status bar is updated. +.. Test case: `cd` + + Expected: User is unable to traverse to any directory. Error message is shown in the status message. The status bar remains the same. +.. Other incorrect cd commands to try: `cd C://xxxx`, `cd xxxx` (where xxxx is a non-existent directory) + + Expected: Similar to previous. + +. `Tab` function to auto-complete directory name + +.. Prerequisites: Users is aware of the current directory they are in from the status bar footer. +.. Test case: `cd Desk` + + Expected: `cd Desktop` is shown in the command box. +.. Test case (only for Windows): `cd C://Use` + + Expected (only in Windows): `cd C://Users` is shown in the command box. +.. Test case: `cd 12345` + + Expected: User does not observe any changes. +.. Other incorrect cd commands to try: `cd C://xxxx`, `cd xxxx` (where xxxx is a non-existent directory) + + Expected: Similar to previous. -=== Deleting a person +=== Opening an image -. Deleting a person while all persons are listed +. Opening images in current directory -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -.. Test case: `delete 1` + - Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. -.. Test case: `delete 0` + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -.. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size) _{give more}_ + +.. Prerequisites: `cd` into a existing directory with at at least 5 images to view the first batch of images in the directory. +.. Test case: `open 1` + + Expected: The first image in the `Film Reel` is displayed on the GUI. The image opened is shown in the status message. +.. Test case: `open 3` + + Expected: The third image in the `Film Reel` is displayed on the GUI. The image opened is shown in the status message. +.. Test case: `open 0` + + Expected: The user is unable to open the image. Error message is shown in the status message. +.. Other incorrect open commands to try: `open 11` (as index exceeds batch size of 10), `open x` (where x exceeds the total images in the current batch), + `open abc` + Expected: Similar to previous. -_{ more test cases ... }_ +=== Traversing between image batches + +. Accessing the next/previous batch of images + +.. Prerequisites: `cd` into a existing directory with 11-20 images to view the first batch of 10 images in the directory. +.. Test case: `next` + + Expected: The next 10 images in the directory is shown in the `Film Reel`. The current batch and remaining number of images is shown in the status message. +.. Test case: `next` + + Expected: The user is unable to access the next batch of images as it exceeds the total number of images in the list. Error message is shown in the status message. +.. Test case: `prev` + + Expected: The first 10 images in the directory is shown in the `Film Reel`. The current batch and remaining number of images is shown in the status message. +.. Test case: `prev` + + Expected: The user is unable to access the prev batch of images as it is already at the start of the list. Error message is shown in the status message. +.. Other incorrect next commands to try: `prev` (when list the already at the start), `next` (when user is viewing the end of list) + + Expected: Similar to previous. -=== Saving data +=== Authenticating with Google Photos -. Dealing with missing/corrupted data files +. Logging in and out of Google Photos + +.. Test case: `login`, then authenticate with Google + + Expected: Logged in email shown in console. +.. Test case: `login`, then close login window without logging in + + Expected: The Piconso application should work smoothly as usual +.. Test case: `logout` when an account is logged into + + Expected: Feedback user is logged out +.. Test case: Launch application when user has not logged out + + Expected: User is automatically logged in as indicated in StatusBarFooter. + +=== Listing with Google Photos + +.. Prerequisites: User is logged in. + +.. Test case: `g ls` + + Expected: Lists names of all photos in account. +.. Test case: `g ls /a` + + Expected: Lists names of all albums in account. +.. Test case: `g ls ` + + Expected: Lists names of all images in that album. +.. Test case: `g ls ` + + Expected: Error message is shown stating album not found. +.. You may also try the methods without <> to test for validity, and also for duplicate image/album names to ensure they list properly. + +=== Downloading with Google Photos + +.. Prerequisites: User is logged in. + +.. Test case: `g dl` + + Expected: Error should be thrown requesting parameter +.. Test case: `g dl /i` + + Expected: Downloads the image +.. Test case: `g dl /a /i` + + Expected: Downloads the image found in the specified album. +.. Test case: `g dl /a` + +Expected: Downloads the entire album +.. You may also try the methods without <> to test for validity and also test with a variety of non-existent albums and image names to ensure +the requests throw an accurate error message. + +=== Downloading with Google Photos + +.. Prerequisites: User is logged in. + +.. Test case: `g ul` + + Expected: Error should be thrown requesting parameter +.. Test case: `g ul ` + + Expected: Uploads the image +.. Test case: `g ul all` + + Expected: Uploads all images in current directory to Google Photos. +.. Test case: `g ul all`, in a folder with 2 images copies + + Expected: Uploads only one image. +.. Test case: `g ul all`, an image that already exists in Google Photos + + Expected: Uploads the image as the name it already had in Google Photos. +.. You may also try the methods without <> to test for validity and also test with a variety of image names and duplicate images to ensure +the requests throw an accurate error message. + +=== Applying transformation to image + +. Apply build in transformation to image + +.. Prerequisites: `cd` into a existing directory with at at least 1 image to view the first batch of images in the directory. +.. Test case: `apply blur 0x8` + + Expected: the image in the preview panel is blurred, the `blur 0x8` is shown in the history panel +.. Test case: `apply rotate 180` + + Expected: the image in the preview panel is rotated 180 degrees, the `rotate 180` is shown in the history panel +.. Test case: `apply blur` + + Expected: the image in the preview panel does not change, invalid message will be shown, command usage will be shown. + +=== Applying raw transformation to image + +. Apply raw transformation to image + +.. Prerequisites: `cd` into a existing directory with at at least 1 image to view the first batch of images in the directory. +.. Test case: `apply raw blur 0x8` + + Expected: the image in the preview panel does not change, invalid operation message will be shown, nothing will be shown in the history panel. +.. Test case: `apply raw -blur 0x8` + + Expected: the image in the preview panel is blurred, the `[-blur 0x8]` is shown in the history panel. + +=== Creating new transformation + +. Create a new transformation + +.. Test case: `create blurSample blur|0x8` + + Expected: message for new transformation created will be shown. +.. Test case: `create blurSample blur` + + Expected: invalid argument message will be shown, command usage message will be shown. + +=== Apply a customised transformation + +. Apply a customised transformation +.. Prerequisites: customised transformation `blurSample` has been created already. +.. Test case: `apply @blurSample` + + Expected: the image in the preview panel is blurred, the `@blurSample` is shown in the history panel. +.. Test case: `apply blurSample` + + Expected: the image in the preview panel does not change, the invalid operation message will be shown. + +=== Undoing and Redoing transformations + +. Undoing transformations +.. Positive Tests +... Prerequisites: An image has been opened, with several transformations applied. +... Test case: `undo` + + Expected: The image's latest transformation in the current layer is undone. +... Test case: `undo-all` + + Expected: All of the image's transformations in the current layer are undone (current layer in original state). +.. Negative Tests +... Prerequisites: An image has been opened, with no transformations applied. Or image has transformations applied, but all are undone. +... Test case: `undo` + + Expected: Error message to user as there are no transformations to undo. +... Test case: `undo-all` + + Expected: Error message to user as there are no transformations to undo. + +. Redoing transformations +.. Positive Tests +... Prerequisites: An image has been opened, with several transformations applied and several transformations undone. +... Test case: `redo` + + Expected: The image's last undone transformation in the current layer is redone. +... Test case: `redo-all` + + Expected: All of the image's undone transformations in the current layer is redone. +.. Negative Tests +... Prerequisites: An image has been opened, with several transformations applied and no transformations undone. Or image is opened with no transformations applied. +... Test case: `redo` + + Expected: Error message to user as there are no transformations to redo. +... Test case: `redo-all` + + Expected: Error message to user as there are no transformations to redo. + +=== Save a image + +. save a edited image +.. Prerequisites: some transformations have been applied to an image. +.. Test case: `save` + + Expected: The modified image will be save to the original image. +.. Test case: `save test.jpg` + + Expected: A new image test.jpg will be created in the current directory. The modified image will be saved to test.jpg. +.. Test case: `save test.test` + + Expected: A invalid format message will be shown, and the image will not be saved. + +=== Canvases and Layers +. Canvas manipulation + +.. Positive cases + +... Test case: `canvas size 1000x1000` + + Expected: The canvas should now a size of 1000 by 1000 pixels. + +... Test case: `canvas bgcolor none` + + Expected: The canvas should now have a transparent background. If the canvas is too small, the background shouldn't even be visible. + +... Test case: `canvas auto-resize on` + + Expected: The canvas should now expand to ensure that all layers are within the frame. The canvas should ignore all options provided by `size`. + +.. Negative cases +... Test case: `canvas bgcolor invalid` + +... Test case: `canvas size -123x456` + +... Test case: `canvas auto-resize invalid` + +Expected: the help message demonstrating proper usage of the command should show up in the console output panel. + +. Layer manipulation + +.. Positive cases +... Test case: `layer add 1` + + Expected: The first image in the `Film Reel` is added as a new layer with on top of all existing layers. + +... Test case: `layer remove 2` + + Expected: The second layer in the canvas should be removed if the following conditions are satisfied: +.... The layer index provided is valid. +.... The layer is not currently selected. +.... The layer is not the last one in the canvas. + + +... Test case: `layer select 2` + + Expected: The layer at index 2 is selected to be the current working layer, highlighted in the `Layers Panel` and be the target of all subsequent transformations if the following condition is satisfied: +.... The layer index provided is valid. + +... Test case: `layer swap 1 2` + + Expected: The layers at index 1 and 2 should be swapped if the following conditions are satisfied: +.... The layer indexes provided are valid. +.... The layer indexes provided are distinct. + +... Test case: `layer position -400x400` + + Expected: The top-left corner of the layer will be set to the coordinate provided. If the coordinates are too far out of the canvas and `auto-resize` isn't turned on, clipping or complete invisibility should occur. Note that negative coordinates are accepted and this is intended behavior. + +.. Negative cases +... Test case: `layer add 11`. + +... Test case: `layer remove 1` when there is only one layer left in the canvas. +... Test case: `layer remove 2` when index 2 is the layer that is selected. +... Test case: `layer swap -1 -3`. +... Test case: `layer swap 1 1`. +... Test case: `layer position invalid`. + + Expected : A specific error message about the constraint violated should show up in the console output panel and no other operations should be performed. -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ -_{ more test cases ... }_ diff --git a/docs/DummySearchPage.html b/docs/DummySearchPage.html deleted file mode 100644 index 1607d4c57291..000000000000 --- a/docs/DummySearchPage.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - Dummy Search Page - - - - Hi : This is a placeholder page for se-edu/addressbook-level4.
- You may update the code to load a page from a real service (e.g., Google search).
- This dummy page is used here because, given the high number of forks of this repo, loading a page from a real third-party service by default can result in that service taking counter-measures (e.g., redirecting to captcha pages) due to the high number of rapid requests received from a single IP.
- When you have made the change, please remove: -
    -
  1. This file (docs/DummySearchPage.html).
  2. -
  3. Task copyDummySearchPage in both build.gradle and .travis.yml.
  4. -
- - diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..8e0fbc03c867 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += Piconso - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,249 +12,680 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103-AY1819S1-T09-3/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `CS2103-AY1819S1-T09-3`      Since: `Aug 2018`      Licence: `MIT` == Introduction -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +Piconso is an image-editing desktop application that supports popular file formats (gif, png, tiff and jpg). *Optimized for those who are familiar with Command Line Interface (CLI)*. If you can type fast, you may be able edit your images even faster than if you were on Photoshop! +Even new users that are unfamiliar with CLI will be able to easily and quickly pick up image-editing with our simple to understand interface. Interested? Jump to the <> to get started. Enjoy! == Quick Start . Ensure you have Java version `9` or later installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. -. Double-click the file to start the app. The GUI should appear in a few seconds. -+ -image::Ui.png[width="790"] -+ -. Type the command in the command box and press kbd:[Enter] to execute it. + -e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. -. Some example commands you can try: - -* *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. -* **`delete`**`3` : deletes the 3rd contact shown in the current list -* *`exit`* : exits the app - -. Refer to <> for details of each command. +. Download the latest `piconso.jar` link:https://github.com/CS2103-AY1819S1-T09-3/main/releases[here]. +. Copy the file to the folder you want to use as the home folder for your Piconso. +. Double-click the file to start the app. The Graphical User Inteface (GUI) should appear in a few seconds. + +image::Ui-Clue.png[width="800"] + +// tag::getting_started1[] +== Getting started + +Let's work through a simple project in Piconso from start to finish. +In this tutorial, you will learn how to create some of the dankest memes starting from a blank template. + +If you ever get stuck with a command, pop down to the Features section for reference! + +.The Piconso team knows how it feels. +image::tutorial/before-after.png[width:2000] + + +**Ready to start your Piconso journey?** + +. Head over to the link https://github.com/CS2103-AY1819S1-T09-3/main/blob/master/docs/images/tutorial/tutorial.zip?raw=true[here] to get a zip file with all the assets you need to get started! Alternatively, copy this link https://github.com/CS2103-AY1819S1-T09-3/main/blob/master/docs/images/tutorial/tutorial.zip?raw=true into your browser of choice. +. Unzip the files into somewhere easy to access, like your Pictures folder. +. Start Piconso! +. Use the `cd` and `ls` commands to get to the folder with your unzipped assets. Your current file location is as noted on the bottom right of the application. +You can use the `TAB` key to auto-complete pesky folder names! If everything goes well, you should be able to see all the images show up in the Image Panel on the lower left. + +.Images and their corresponding indexes are showing up! +image::tutorial/step 1.png[width:2000] + +[start=5] +. See the number below the base image? That's the __index__ of the image. It might be different depending on your folder's content. If you don't see it within the first ten images, use the `next` and `prev` command to navigate! +. Open the image simply by typing `open [index]`. __(Substitute index with the number you noted down just now)__ +. You should see the base template in all its glory. + +// end::getting_started1[] + +// tag::getting_started2[] + +.Ready to rumble. +image::tutorial/step 2.png[width:2000] + +[start=8] +. Now we can start layering the other images on top of the base image. Let's start by adding the coveralls logo. +We do that by using the `layer add` command. The logo is at index 3 for us so we enter `layer add 3`. + +.Not exactly where we want it but it's a start. +image::tutorial/step 3.png[width:2000] + +[start=9] +. The logo is a bit too big for our liking so let's resize it a bit. Start by selecting the layer that the logo is on with a quick `layer select 2`. +. Follow up with `apply resize 70%` and put Coveralls down a peg or two. + +.Why don't you pick on someone your own **SIZE** huh? Get it? Size? +image::tutorial/step 4.png[width:2000] + +[start=11] +. The logo isn't where we want it so let's move it right to the balloon. +Hit it with a `layer position 295x50`. + +.And that's for rejecting my pull request! +image::tutorial/step 5.png[width:2000] + +[start=12] +. Repeat steps 8 - 11 to place `regret.png` somewhere on the pink monster. Remember to select the new layer before you start to resize and reposition! __(Psst. resize for 60% and move it to (5, 450) if you don't feel like experimenting.)__ + +.Do androids dream of 100% code coverage? +image::tutorial/step 6.png[width:2000] + +[start=13] +. Let's save our work before we **DEEP FRY** the living daylights out of it! You can do that by typing `save arrangement.png` or if you're feeling adventurous just `save` will overwrite your current file irreversibly. +. Open our file again, refer to step 5 and 6 if you need help. +. Now we can begin to use some of the more advanced features in Piconso! We add some noise to the image with `apply raw +noise gaussian`. +. Feel free to repeat the step 15 a few times to get the desired amount of noise! We did it five times to be sure. If you ever go overboard, use the `undo` command to ... undo the previous command. But face it, you're going to `redo` it anyway. + +.Turn down for what? +image::tutorial/step 7.png[width:2000] + +[start=17] +. Now we bump the contrast up, **way __way__** up with `apply raw -contrast-stretch 15%`. +. Brightness and saturation gets the same boost with `apply raw -modulate 120,120`. +. Apply the finishing touches with an unsharp mask! `apply raw -unsharp 0x05`. Now the meme is starting to take shape! + +.New and improved image, now with 500% more noise, 120% more brightness, 120% more saturation! +image::tutorial/step 8.png[width:2000] + +[start=20] +. We are almost done! Repeat steps 8 - 11 again to slap some of the emojis on. +You can use the `apply rotate [degree]` command to spice the meme up a bit. If you're short on time, we've provided the exact resize and new positions: + + For the crying emoji: + layer position 125x330 + apply resize 20% + apply rotate 75 + + For the 2 OK emojis: + apply resize 10% + layer position 270x390 + apply rotate 80 + +.You're finna gonna yeet this image. +image::tutorial/step 9.png[width:2000] + +[start=21] +. Save the image again (`save lit_meme.png`) and we're ready to upload it! +. Login with the aptly named `login` command. Don't worry, Piconso will never store your password in any shape or form! +. Upload the file with `g ul `. You should see a confirmation message when the upload is complete. +. `logout` when you're done! Congrats, you have just went from zero to a Piconso-certified meme hero! + +Before we let you go, wonder how we made the very first image you saw? Try this before you go! + +[start=25] +. Add the base image back in, the base image will take up all the canvas space but don't worry. +. Select the layer which the original image is on and move it out of the way + + layer select 5 + layer position -450x0 + +[start=28] +. Use `canvas auto-resize on` to expand the canvas to show both of them. +. Remove the background color of the canvas with `canvas bgcolor none` or change it to something you like! + +// end::getting_started2[] [[Features]] == Features +=== Command Format +* Items in square brackets are compulsory parameters to be added e.g `apply [TRANSFORMATION]` + +* Items in round brackets are optional parameters and can be omitted, e.g `canvas size (NEW_SIZE)` + +* For items surrounded by `< >`, they are compulsory parameters to be added that need to be typed with `< >` + +** e.g `g ls ` can be used as `g ls ` + +* Flags such as `/a` are compulsory flags that need to be included when using that command +** e.g `g ls /a` + +=== Views help : `help` + +Format: `help` -> Shows a list of all commands. + +=== Lists files in directory : `ls` + +Format: `ls` -> Lists all images and folders located in the current directory + +// tag::dir[] +=== Changes directory : `cd` + +Format: `cd [DIRECTORY_NAME]` -> Changes to specified directory + +[NOTE] ==== -*Command Format* +For changing of drives in Windows, add '//' after the drive name. In addition, pressing `Tab` key will autocomplete the directory name. +==== + +Example: + +* `cd C://Users` - Changes the directory to Users in C:/ drive. + +=== Opens images for editing: `open` + +Format: `open [INDEX 1-10]` -> Opens an image for image-editing + +Format: `open -all` -> Adds all batch images in currently opened directory as layers. + +Examples: + +* `open 1` + +* `open -all` [Coming in v2.0] + +=== Retrieves the next batch of images: `next` + +[NOTE] +The image panel to the bottom left can only show 10 images at once, and by default will be showing the first 10 images in your directory. + +Format: `next` -> Retrieves the next 10 images for editing + +=== Retrieves the previous batch of images: `prev` + +Format: `prev` -> Retrieves the previous 10 images for editing + +// end::dir[] +// tag::apply[] + +=== Applies a transformation to an image : `apply` +There are 3 different ways to apply a transformation to an image. + + +Format: `apply [operation] [arg1] [arg2] ...` -> Applies the specified transformation to the selected layer (or the current image if there is only one layer). + +Format: `apply @Operation` -> Applies a custom transformation to the image, `@` is compulsory. + + +There are many different transformations available, which we will briefly describe: + +==== `blur`: Blurs your entire layer accordingly to the specified radius & sigma values. +* _Format_: `apply blur [RadiusxSigma]` +** Radius (0-99): Helps in blur calculation, recommended to have a value at least twice of Sigma +** Sigma (0-99): Determines the extent of the blur +* _Example_: `apply blur 16x8` + +==== `motion-blur`: Simulates a motion blur on the layer +* _Format_: `apply motion-blur [RadiusxSigma+Angle]` +** Radius (0-29): Helps in blur calculation, recommended to have a value at least twice of Sigma +** Sigma (0-19): Determines the extent of the blur +** Angle (0-99): Direction of the blur +* _Example_: `apply motion-blur 0x8+45` + +==== `colorspace`: Changes the color range of the current layer +* _Format_: `apply colorspace [color space]` +** color space (sRGB/RGB/GRAY/LAB/CMYK): Changes the range of colors allowed on the image. Can be typed as upper/lower case. +* _Example_: `apply colorspace GRAY` + +[NOTE] +For some color spaces, the image may appear to remain the same even after after changing color spaces. + +==== `contrast`: Enhances the intensity differences between the lighter and darker elements +* _Format_: `apply contrast` +* _Example_: `apply contrast` + +[NOTE] +The effect of `contrast` may not very obvious depending on the image you are using. +You may repeat the transformation again or use `sigmodial-contrast` instead if want further enhance the effect. + +==== `sigmoidal-contrast`: Contrasts the image without saturating highlights or shadows. +* Format: `apply sigmoidal-contrast [ContrastxMidpoint]` +** Contrast (1-99): How much to increase the contrast. +** Midpoint (1%-99%): Where the maximum change 'slope' in contrast should fall in the resultant image (0 is white; 50% is middle-gray; 100% is black). +** Example: `apply sigmoidal-contrast 10x10%` + +==== `noise`: Increases the noise in the image +* _Format_: `apply noise [Radius]` +** Radius (0-29): Determines the width of the affected area +* _Example_: `apply noise 12` + +==== `resize`: Resizes the image +* _Format_: `apply resize [percentage]` +** Percentage (1%-399%): Percentage of resize relative to the original image. +* _Example_: `apply resize 50%` + +[NOTE] +As all the display image will be resized to fit the panel when displayed, the `resize` command may appear to have taken no effect when used on a canvas with only one layer. + + +==== `rotate`: Rotates an image +* _Format_: `apply rotate [value]` +** Value (-360 to 360): Degree to rotate the image by. Negative values rotate the image to the anti-clockwise. +* _Example_: `apply rotate 90` + +[NOTE] +The image will rotate about its top-left corner! + +==== `sharpen`: Sharpens the currently opened image + +** _Format_: `apply sharpen [RadiusxSigma]` +** Radius (0-29): Helps in sharpen calculation. +** Sigma (0-29): Determines the extent of the sharpen +** _Example_: `apply sharpen 0x8` -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +==== *Advanced `apply` commands* + +** Format: `apply raw [arg1] [arg2]` -> Directly passes arguments entered to ImageMagick. You may refer to https://www.imagemagick.org/script/command-line-options.php[guide] to view all operations. + +[WARNING] +**Here be dragons!** The raw command allows for powerful transformations but with power comes great responsibility to get your commands right. While we can tell you if an error occurs, we can't help you more than that so be careful! + +// end::apply[] + +// tag::create[] + +=== Creates custom transformation sets for use : `create` + +Format: `create [Name_Of_operation] [op1|arg1|arg2|...] [op2|arg1|arg2|...]`: Allows you to create your own custom transformation based on the transformations specified + +Example: + +* `create blurAndRotate blur|0x8 rotate|90` + +** To use the newly created command, add an `@` before the command used -> `apply @blurAndRotate` + +// end::create[] +// tag::save[] + +=== Saves images : `save` + +Format: `save (IMAGE_NAME)`: Saves the transformed image. Supports file formats jpg, jpeg, png, tiff, gif + +Example: + +* `save modified.png` - Saves the image into currently opened directory as `modified.png` +* `save` - Overwrites the original image in the currently opened directory. (saved as its original name) + +// end::save[] +// tag::undoredo[] + +=== Undoes a transformation : `undo` + +Format: `undo` -> Undoes your previous transformation, putting your image to its previous state (in current layer) + +[NOTE] +==== +Only commands that modify the image's transformation (anything done with `apply`) can be undone. ==== -=== Viewing help : `help` +Example: + +`apply blur 0x8` + +`apply contrast` + +`undo` -> (undoes the `apply contrast` command, image will be returned to be `blur 0x8` state) + + +=== Undoes all transformations : `undo-all` + +Format: `undo-all` -> Undoes all your transformations, reverting your image to its original state + +Example: + +`apply blur 0x8` + +`apply contrast` + +`apply rotate 90` + +`undo-all` -> undoes all 3 `apply` commands, image will be at original state + +=== Redoes a transformation : `redo` -Format: `help` +Format: `redo` -> Reapplies your previously undone transformation, putting your image in its next state (in current layer) -=== Adding a person: `add` +Example: -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +`apply blur 0x8` + +`apply contrast` + +`undo` -> (undoes the `apply contrast` command, image will be at `blur 0x8` state) + +`redo` -> (reapplies the `apply contrast` command) + -[TIP] -A person can have any number of tags (including 0) +=== Redoes all transformations : `redo-all` + +Format: `redo-all` -> Reapplies all your undone transformations, putting your image in the state with all transformations applied + +Example: + +`apply blur 0x8` + +`apply contrast` + +`apply rotate 90` + +`undo-all` (undoes all 3 `apply` commands, image will be at original state) + +`redo-all` (reapplies all 3 `apply` commands) + +// end::undoredo[] + +// tag::canvas[] +=== Canvas operations : `canvas` +[NOTE] +==== +Canvas operations are not transfomations and unaffected by `undo/redo/undo-all/redo-all` commands! +==== +==== Changes the size of the canvas: `canvas size` +Format: `canvas size (NEW_SIZE)` -> You can change the size of the canvas to with this command. Remember that the effects of `canvas auto-resize` takes precedence! + +If `canvas auto-resize` is `off`, cropping might potentially occur. + +[NOTE] +==== +If the optional parameter NEW_SIZE is not provided, the current size will be displayed in the output instead. +==== Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `canvas size 800x600` - Sets the canvas to have a height of 800 pixels and a width of 600px. +* `canvas size` - Prints the current size. -=== Listing all persons : `list` +==== Changes the background color of the canvas: `canvas bgcolor` +Format: `canvas bgcolor (NEW_COLOR)` -> You can change the background color of the canvas with this command. This is only visible if the canvas is larger than all the images placed on it. +See the examples for the formats that your colors can take. -Shows a list of all persons in the address book. + -Format: `list` +[NOTE] +==== +If the optional parameter NEW_COLOUR is not provided, the current colour will be displayed in the output instead. +==== + +Examples: + +* `canvas bgcolor none` - Sets the canvas to have a transparent background. +* `canvas bgcolor #0f0` - Sets the canvas to the hex colour #00ff00 image:00ff00.png[width:15]. +* `canvas bgcolor #00ff00` - Sets the canvas to the hex colour #00ff00 image:00ff00.png[width:15]. +* `canvas bgcolor rgba(0,255,0,0.7)` - Sets the canvas to the hex colour #00ff00 but with 70% opacity image:00ff00-70.png[width:15]. +* `canvas bgcolor` - Prints the background colour. -=== Editing a person : `edit` +==== Allowing the canvas to auto-resize: `canvas auto-resize [ON|OFF]` +Format : `canvas auto-resize [ON|OFF]` -> This command allows you to turns the auto-resize for the canvas on or off. -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` -**** -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index *must be a positive integer* 1, 2, 3, ... -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person's tags by typing `t/` without specifying any tags after it. -**** +When auto-resize is on, it can potentially override the size manually specified with the `canvas size` command. +When `on`, the canvas expands as required to ensure that no clipping occurs. + +[NOTE] +==== +New canvases default to having auto-resize off. +==== Examples: -* `edit 1 p/91234567 e/johndoe@example.com` + -Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` + -Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `canvas auto-resize on` - Allows the canvas to expand and prevent cropping. +* `canvas auto-resize off` - The height and width of the output canvas will remain as is. -=== Locating persons by name: `find` +// end::canvas[] -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +// tag::layer[] +=== Layer operations : `layer` +[NOTE] +==== +Layer operations are not transfomations and unaffected by `undo/redo/undo-all/redo-all` commands! +==== +==== Adds a new layer: `layer add [INDEX] (LAYER_NAME)` +Format : `layer add [INDEX] (LAYER_NAME)` -> You can select the image at the provided index and add it to the current canvas with this command. -**** -* The search is case insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` -**** +[NOTE] +==== +If the optional parameter LAYER_NAME is not provided, a name will be automatically generated. +==== Examples: -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +* `layer add 4` - Adds the image at index 4 to the canvas. -=== Deleting a person : `delete` +==== Deletes a layer: `layer delete [INDEX]` +Format : `layer delete [INDEX]` -> This command allows you to permanently deletes a layer from canvas. -Deletes the specified person from the address book. + -Format: `delete INDEX` +[WARNING] +==== +This operation is not reversible! Be careful when removing layers! +==== -**** -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* 1, 2, 3, ... -**** +==== Selects a layer to work on: `layer select [INDEX]` +Format : `layer select [INDEX]` -> This command allows you to a layer to that all `apply` and `layer position` operations will work on. The index of a layer is displayed before its name in the Layers panel. Examples: -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. +* `layer select 2` : Selects the layer at index 2. -=== Selecting a person : `select` +==== Swaps the order of two layers: `layer swap [TO_INDEX] [FROM_INDEX]` +Format : `layer swap [TO_INDEX] [FROM_INDEX]` -> You can change the order of any two distinct layers with this command. The lower the index, the further behind it will be. -Selects the person identified by the index number used in the displayed person list. + -Format: `select INDEX` +Examples: -**** -* Selects the person and loads the Google search page the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* `1, 2, 3, ...` -**** +* `layer swap 1 2` - Changes the order of layer 1 and layer 2. + +==== Positions a layer: `layer position [POSITION]` +Format : `layer position [POSITION]` -> This command allows you to set the x and y co-ordinates of the current layer. (0,0) is defined to be the top left of the screen. + +[WARNING] +==== +The provided x and y co-ordinates must be between –2147483648 and 2147483647! +==== Examples: -* `list` + -`select 2` + -Selects the 2nd person in the address book. -* `find Betsy` + -`select 1` + -Selects the 1st person in the results of the `find` command. +* `layer position 50x100` - Sets the layer's top left corner to be at position (50, 100). + +// end::layer[] + +// tag::google[] + +=== Interact with Google Photos: `g` +Piconso allows you to connect to Google Photos so that you may easily upload and download photos for editing. -=== Listing entered commands : `history` +All Google-related commands will require a stable internet connection. Additionally as the following are internet-dependant, the commands will take a longer amount of time to execute as compared to the other offline features. -Lists all the commands that you have entered in reverse chronological order. + -Format: `history` +Connecting to Google Photos is *NOT* compulsory, it is simply an additional feature to supplement your experience with Piconso! [NOTE] ==== -Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +The higher the amount of images involved in your google command, the longer amount of time commands will take to process. ==== -// tag::undoredo[] -=== Undoing previous command : `undo` +==== Logs in to Google Photos: `login` +Format : `login` -> Allows you to connect to Google Photos through a browser redirect + +==== Traverses Google Photos: `g ls` +Format: `g ls (parameters)` -> Lists the images you have in Google Photos. + +There are 3 ways you can list your images. -Restores the address book to the state before the previous _undoable_ command was executed. + -Format: `undo` +* `g ls`: Lists all images in your account + +* `g ls /a`: Lists all your available albums + +* `g ls `: Lists all images in specified album from Google Photos. + +Examples: + +`* g ls ` - Lists all images in the Vacation album. + +==== Downloads photos from Google Photos: `g dl` +Format: `g dl [parameters]` -> Downloads your specified image from Google Photos. [NOTE] ==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +All photos will be downloaded to the currently opened local directory. + +Any files with duplicate naming existing in the targeted directory *WILL* be replaced ==== -Examples: +There are 3 ways you can list your images: -* `delete 1` + -`list` + -`undo` (reverses the `delete 1` command) + +* `g dl /i` : Downloads the specified image from Google Photos + +* `g dl /a` : Downloads all images from the specified album into Google Photos. + +* `g dl /a /i` : Downloads a specific photo from a specific album in Google Photos. -* `select 1` + -`list` + -`undo` + -The `undo` command fails as there are no undoable commands executed previously. +Examples: -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + +* `g dl /i` - Downloads Beach.png + +* `g dl /a` - Downloads all photos from Vacation album + +* `g dl /a /i` - Downloads Beach.png from Vacation album + -=== Redoing the previously undone command : `redo` +==== Uploads photos to Google Photos: `g ul` +Format: `g ul [parameters]` -> Uploads your specified image to Google Photos. -Reverses the most recent `undo` command. + -Format: `redo` +[NOTE] +==== +All uploaded photos will be automatically categorised into album 'Piconso Uploads' in Google Photos. Any duplicate images in Google Photos will be replaced by the new upload with the old name. +==== +There are 2 ways you can list your images: + +* `g ul ` : Uploads the specified image to Google Photos + +* `g ul all` : Uploads all images in current opened directory to Google Photos. Examples: -* `delete 1` + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + +* `g ul ` - Uploads Cat.png + -* `delete 1` + -`redo` + -The `redo` command fails as there are no `undo` commands executed previously. +=== Logs out of Google Photos: `logout` -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + -`redo` (reapplies the `clear` command) + -// end::undoredo[] +Format: `logout` -> Disconnects you from Google Photos + +// end::google[] + +=== Exits the application: `exit` -=== Clearing all entries : `clear` +Format: `exit` -> Closes the application -Clears all entries from the address book. + -Format: `clear` +== Upcoming Features (in v2.0) -=== Exiting the program : `exit` +=== Batch Apply -Exits the program. + -Format: `exit` +Format: `b-apply [operation] (arg1) (arg2)` -> Applies the transformation to all images in the current directory and saves them as a copy of themselves. -=== Saving the data +Example: -Address book data are saved in the hard disk automatically after any command that changes the data. + -There is no need to save manually. +In a directory with `y.png`, `z.png`. -// tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +* `b-apply colorspace gray` -> Transformation applied to both images in directory and they are saved as `y_edit.png` and `z_edit.png` -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] +=== Export + +Format: `export [SYSTEM] (FOLDER_NAME)` -> Batch exports the images into sizes suitable for the OS stated into a new folder in the current directory (if specified). This is especially useful if you're a developer for mobile environments. + +* _Parameter_ : `[System]` +** Android: Exports to hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi sizes +** iOS: Exports to 1x, 2x, 3x of original size. + +Examples: + +* `export android` - Saves canvas into sizes hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi in currently opened directory + +* `export android AND` - Saves canvas into sizes hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi into a newly created folder `AND` in currently opened directory + + +=== Ruler + +Format: `ruler [ON|OFF]` -> Toggles a ruler that surrounds the canvas. This will help you in estimating the number of pixels to move when using `layer position` or resizing objects. == FAQ *Q*: How do I transfer my data to another Computer? + -*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous Address Book folder. +*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous folder. == Command Summary -* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + -e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -* *Clear* : `clear` -* *Delete* : `delete INDEX` + -e.g. `delete 3` -* *Edit* : `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + -e.g. `edit 2 n/James Lee e/jameslee@example.com` -* *Find* : `find KEYWORD [MORE_KEYWORDS]` + -e.g. `find James Jake` -* *List* : `list` -* *Help* : `help` -* *Select* : `select INDEX` + -e.g.`select 2` -* *History* : `history` -* *Undo* : `undo` -* *Redo* : `redo` + +*LEGEND* + +* `[PARAMETER]` -> Represents a compulsory parameter +* `(PARAMETER)` -> Represents an optional parameter +* `` -> Represents a parameter that must be typed with `< >` +* `/a, /i, @` -> Compulsory flags that must be typed. +[width="70%",cols="<20%,<32%,48%",options="header",] +|======================================================================= +|Feature |Format |Purpose +|*Help* |`help` |Pulls up the help guide. + +|*List* |`ls` |Lists all images and folders in current directory + +|*Change directory* |`cd [DIRECTORY_NAME]` | Moves to the specified directory + +|*Open image* |`open [IMAGE_INDEX]` | Opens the image associated with the index + +|*Show next batch* |`next` | Moves to the next batch of 10 images in the current directory. + +|*Show previous batch* |`prev` | Moves to the previous batch of 10 images in the current directory. + +|*Apply* |`apply [TRANSFORMATION]` | Applies a transformation to the image (table below shows all available transformations) + +|*Save* |`save [IMAGE_NAME]` | Saves the image as the name provided + +|*Create* |`create [newName] [op1 \| arg1 \| arg2 \|...] [op2 \| arg1 \| arg2 \| ...]` | Creates a custom transformation + +|*Undo* |`undo` | Undo most recent transformations on current layer + +|*Undo all* |`undo-all` | Undoes all transformations on current layer + +|*Redo* |`redo` | Redo most recent transformation on current layer + +|*Redo all* |`redo-all` | Redoes all undone transformation on current layer + +|*Canvas auto-resize* |`canvas auto-resize [ON/OFF]` | Toggles the canvas auto-resize. + +|*Set background color* |`canvas bgcolor [COLOR]` | Changes the color of the background of the canvas. + +|*Changes the canvas size* |`canvas size (HEIGHTxWIDTH)` | Changes the size of the canvas. + +|*Add layer* |`layer add [INDEX]` | Adds the selected image index as a new layer. + +|*Delete layer* |`layer delete [INDEX]` | Deletes the layer at the selected index. + +|*Reposition layer* |`layer position [XPOSxYPOS]` | Repositions the layer to the specified position. + +|*Select layer* |`layer select [INDEX]` | Changes to layer at the selected index. + +|*Swap layers* |`layer swap [INDEX 1] [INDEX 2]` | Swaps the order of the two layers. + +|*Login* |`login` | Prompts you to login to Google Photos (GP). + +|*List images on GP* |`g ls` | Lists the photos you have on GP. + +|*List albums on GP* |`g ls /a` | Lists the albums you have on GP. Try `g ls ` to list images from a specific album + +|*Download image* |`g dl /i` | Allows you to download specified image from GP + +|*Download album* |`g dl /a` | Allows you to download an entire album from GP. Try `g dl /a /i` | Allows you to upload specified image to GP + +|*Upload folder* |`g ul all` | Allows you to upload all images in current directory to GP + +|*Logout* |`logout` | Logs you out of GP + +|*Exit* |`exit` | Exits Piconso + +|======================================================================= + +== Transformation Summary + +* Read Section 4.8 for more details on parameter values + +[width="70%",cols="<20%,<32%,48%",options="header",] +|======================================================================= +|Transformation |Usage |Purpose +|*Custom* |`apply @[CUSTOM_NAME]` |Applies a custom transformation you have created before hand, `@` is compulsory. + +|*Blur* |`apply blur [RadiusxSigma]` Values -> Radius (0-99), Sigma (0-99)|Blurs your entire layer accordingly to the radius & sigma values. + +|*Motion Blur* |`apply motion-blur [RadiusxSigma+Angle]` Values -> Radius (0-29), Sigma (0-19), Angle (0-99)| Simulates a motion blur on the layer + +|*Colorspace* |`apply colorspace [color space] Values -> sRGB/RGB/GRAY/LAB/CMYK`| Changes the range of colors allowed on the image. + +|*Contrast* |`apply contrast`| Enhances the intensity differences between the lighter and darker elements + +|*Sigmoidal Contrast* |`apply sigmoidal-contrast [ContrastxMidpoint]`| Contrasts the image without saturating highlights or shadows. + +|*Noise* |`apply noise [Radius]` Values -> Radius (0-29) | Increases the noise in the image + +|*Resize* |`apply resize [percentage]` | Resizes the image + +|*Rotate* |`apply rotate [value], Value -> (-360 to 360)`| Rotates an image + +|*Sharpen* |`apply sharpen [RadiusxSigma]` Values -> Radius (0-29), Sigma (0-29) | Sharpens the currently opened image + +|*Advanced* |`apply raw [arg1] [arg2]` | Directly passes arguments entered to ImageMagick. + +|======================================================================= + diff --git a/docs/diagrams/ApplyCommandDiagram.pptx b/docs/diagrams/ApplyCommandDiagram.pptx new file mode 100644 index 000000000000..fe342a5a1544 Binary files /dev/null and b/docs/diagrams/ApplyCommandDiagram.pptx differ diff --git a/docs/diagrams/CanvasSequenceDiagram.pptx b/docs/diagrams/CanvasSequenceDiagram.pptx new file mode 100644 index 000000000000..2cf1e4f663ce Binary files /dev/null and b/docs/diagrams/CanvasSequenceDiagram.pptx differ diff --git a/docs/diagrams/CdSequenceDiagram.pptx b/docs/diagrams/CdSequenceDiagram.pptx new file mode 100644 index 000000000000..0aee51d832bc Binary files /dev/null and b/docs/diagrams/CdSequenceDiagram.pptx differ diff --git a/docs/diagrams/CreateTransformationActivityDiagram.pptx b/docs/diagrams/CreateTransformationActivityDiagram.pptx new file mode 100644 index 000000000000..fb3a3a7d4a10 Binary files /dev/null and b/docs/diagrams/CreateTransformationActivityDiagram.pptx differ diff --git a/docs/diagrams/GoogleDlSequenceDiagram.pptx b/docs/diagrams/GoogleDlSequenceDiagram.pptx new file mode 100644 index 000000000000..1604713e2924 Binary files /dev/null and b/docs/diagrams/GoogleDlSequenceDiagram.pptx differ diff --git a/docs/diagrams/GoogleLsActivityDiagram.pptx b/docs/diagrams/GoogleLsActivityDiagram.pptx new file mode 100644 index 000000000000..42058f4d2025 Binary files /dev/null and b/docs/diagrams/GoogleLsActivityDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentClassDiagram.pptx b/docs/diagrams/LogicComponentClassDiagram.pptx index 6fcc1136a5bb..2659c198fd2d 100644 Binary files a/docs/diagrams/LogicComponentClassDiagram.pptx and b/docs/diagrams/LogicComponentClassDiagram.pptx differ diff --git a/docs/diagrams/LogicComponentSequenceDiagram.pptx b/docs/diagrams/LogicComponentSequenceDiagram.pptx index c5b6d5fad6e3..2a0e24dd6be0 100644 Binary files a/docs/diagrams/LogicComponentSequenceDiagram.pptx and b/docs/diagrams/LogicComponentSequenceDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassDiagram.pptx b/docs/diagrams/ModelComponentClassDiagram.pptx index 3c976908eaa7..970ccbd7346f 100644 Binary files a/docs/diagrams/ModelComponentClassDiagram.pptx and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/diagrams/NextSequenceDiagram.pptx b/docs/diagrams/NextSequenceDiagram.pptx new file mode 100644 index 000000000000..30edaf641d20 Binary files /dev/null and b/docs/diagrams/NextSequenceDiagram.pptx differ diff --git a/docs/diagrams/OpenCommand1.pptx b/docs/diagrams/OpenCommand1.pptx new file mode 100644 index 000000000000..e9cc873a2556 Binary files /dev/null and b/docs/diagrams/OpenCommand1.pptx differ diff --git a/docs/diagrams/OpenCommand2.pptx b/docs/diagrams/OpenCommand2.pptx new file mode 100644 index 000000000000..72ca036f0de3 Binary files /dev/null and b/docs/diagrams/OpenCommand2.pptx differ diff --git a/docs/diagrams/OpenCommand3.pptx b/docs/diagrams/OpenCommand3.pptx new file mode 100644 index 000000000000..ff920f5a37ea Binary files /dev/null and b/docs/diagrams/OpenCommand3.pptx differ diff --git a/docs/diagrams/OpenSequenceDiagram.pptx b/docs/diagrams/OpenSequenceDiagram.pptx new file mode 100644 index 000000000000..32aef7c179ec Binary files /dev/null and b/docs/diagrams/OpenSequenceDiagram.pptx differ diff --git a/docs/diagrams/PrevSequenceDiagram.pptx b/docs/diagrams/PrevSequenceDiagram.pptx new file mode 100644 index 000000000000..fc142cf060f8 Binary files /dev/null and b/docs/diagrams/PrevSequenceDiagram.pptx differ diff --git a/docs/diagrams/SaveCommandDiagram.pptx b/docs/diagrams/SaveCommandDiagram.pptx new file mode 100644 index 000000000000..19ed8097d5b5 Binary files /dev/null and b/docs/diagrams/SaveCommandDiagram.pptx differ diff --git a/docs/diagrams/SelectNextCommand2.pptx b/docs/diagrams/SelectNextCommand2.pptx new file mode 100644 index 000000000000..759abeecaae3 Binary files /dev/null and b/docs/diagrams/SelectNextCommand2.pptx differ diff --git a/docs/diagrams/SelectNextCommand3.pptx b/docs/diagrams/SelectNextCommand3.pptx new file mode 100644 index 000000000000..7063d2a5e0f3 Binary files /dev/null and b/docs/diagrams/SelectNextCommand3.pptx differ diff --git a/docs/diagrams/StorageComponentClassDiagram.pptx b/docs/diagrams/StorageComponentClassDiagram.pptx index be29a9de7ca6..751e31872f61 100644 Binary files a/docs/diagrams/StorageComponentClassDiagram.pptx and b/docs/diagrams/StorageComponentClassDiagram.pptx differ diff --git a/docs/diagrams/TransformationActivityDiagram.pptx b/docs/diagrams/TransformationActivityDiagram.pptx new file mode 100644 index 000000000000..5d266dbb1a6b Binary files /dev/null and b/docs/diagrams/TransformationActivityDiagram.pptx differ diff --git a/docs/diagrams/UiComponentClassDiagram.pptx b/docs/diagrams/UiComponentClassDiagram.pptx index 384d0a00e6ea..764e70fb7128 100644 Binary files a/docs/diagrams/UiComponentClassDiagram.pptx and b/docs/diagrams/UiComponentClassDiagram.pptx differ diff --git a/docs/diagrams/UndoRedoActivityDiagram.pptx b/docs/diagrams/UndoRedoActivityDiagram.pptx deleted file mode 100644 index 16fec930cf3f..000000000000 Binary files a/docs/diagrams/UndoRedoActivityDiagram.pptx and /dev/null differ diff --git a/docs/diagrams/UndoRedoSequenceDiagram.pptx b/docs/diagrams/UndoRedoSequenceDiagram.pptx index 5ccc1042caac..96e8256200f8 100644 Binary files a/docs/diagrams/UndoRedoSequenceDiagram.pptx and b/docs/diagrams/UndoRedoSequenceDiagram.pptx differ diff --git a/docs/images/00ff00-70.png b/docs/images/00ff00-70.png new file mode 100644 index 000000000000..50fb3132f7fc Binary files /dev/null and b/docs/images/00ff00-70.png differ diff --git a/docs/images/00ff00.png b/docs/images/00ff00.png new file mode 100644 index 000000000000..d3a420d237ed Binary files /dev/null and b/docs/images/00ff00.png differ diff --git a/docs/images/ApplyCommandSequenceDiagram.png b/docs/images/ApplyCommandSequenceDiagram.png new file mode 100644 index 000000000000..105f1494bf95 Binary files /dev/null and b/docs/images/ApplyCommandSequenceDiagram.png differ diff --git a/docs/images/CanvasLayersDemo.png b/docs/images/CanvasLayersDemo.png new file mode 100644 index 000000000000..961184aa43c6 Binary files /dev/null and b/docs/images/CanvasLayersDemo.png differ diff --git a/docs/images/CanvasSequenceDiagram.png b/docs/images/CanvasSequenceDiagram.png new file mode 100644 index 000000000000..9a0a32275524 Binary files /dev/null and b/docs/images/CanvasSequenceDiagram.png differ diff --git a/docs/images/CdSequenceDiagram.png b/docs/images/CdSequenceDiagram.png new file mode 100644 index 000000000000..608cbbae004e Binary files /dev/null and b/docs/images/CdSequenceDiagram.png differ diff --git a/docs/images/CreateTransformationActivityDiagram.png b/docs/images/CreateTransformationActivityDiagram.png new file mode 100644 index 000000000000..33076aa30940 Binary files /dev/null and b/docs/images/CreateTransformationActivityDiagram.png differ diff --git a/docs/images/GoogleDlSequenceDiagram.png b/docs/images/GoogleDlSequenceDiagram.png new file mode 100644 index 000000000000..abf949180bc8 Binary files /dev/null and b/docs/images/GoogleDlSequenceDiagram.png differ diff --git a/docs/images/GoogleLsActivityDiagram.png b/docs/images/GoogleLsActivityDiagram.png new file mode 100644 index 000000000000..3d8a22cdeadb Binary files /dev/null and b/docs/images/GoogleLsActivityDiagram.png differ diff --git a/docs/images/LayerOperationGuide.png b/docs/images/LayerOperationGuide.png new file mode 100644 index 000000000000..0fd44560ce6a Binary files /dev/null and b/docs/images/LayerOperationGuide.png differ diff --git a/docs/images/LayerSequenceDiagram.png b/docs/images/LayerSequenceDiagram.png new file mode 100644 index 000000000000..38625b4512d6 Binary files /dev/null and b/docs/images/LayerSequenceDiagram.png differ diff --git a/docs/images/LogicClassDiagram1.png b/docs/images/LogicClassDiagram1.png new file mode 100644 index 000000000000..2c46c7788ec4 Binary files /dev/null and b/docs/images/LogicClassDiagram1.png differ diff --git a/docs/images/LogicComponentClassDiagram.png b/docs/images/LogicComponentClassDiagram.png new file mode 100644 index 000000000000..0f4a842c638a Binary files /dev/null and b/docs/images/LogicComponentClassDiagram.png differ diff --git a/docs/images/LoginSequenceDiagram.png b/docs/images/LoginSequenceDiagram.png new file mode 100644 index 000000000000..1ee083583f9c Binary files /dev/null and b/docs/images/LoginSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram_Piconso.png b/docs/images/ModelClassDiagram_Piconso.png new file mode 100644 index 000000000000..c0e27b149fcf Binary files /dev/null and b/docs/images/ModelClassDiagram_Piconso.png differ diff --git a/docs/images/NextPrevCommand2.png b/docs/images/NextPrevCommand2.png new file mode 100644 index 000000000000..789e5789d41b Binary files /dev/null and b/docs/images/NextPrevCommand2.png differ diff --git a/docs/images/NextPrevCommand3.png b/docs/images/NextPrevCommand3.png new file mode 100644 index 000000000000..9f05f17fdf70 Binary files /dev/null and b/docs/images/NextPrevCommand3.png differ diff --git a/docs/images/NextSequenceDiagram.png b/docs/images/NextSequenceDiagram.png new file mode 100644 index 000000000000..1ed049820eb1 Binary files /dev/null and b/docs/images/NextSequenceDiagram.png differ diff --git a/docs/images/OpenCommand2.png b/docs/images/OpenCommand2.png new file mode 100644 index 000000000000..e5e1cf7d2a2f Binary files /dev/null and b/docs/images/OpenCommand2.png differ diff --git a/docs/images/OpenCommand3.png b/docs/images/OpenCommand3.png new file mode 100644 index 000000000000..e085db3244fb Binary files /dev/null and b/docs/images/OpenCommand3.png differ diff --git a/docs/images/OpenSequenceDiagram.png b/docs/images/OpenSequenceDiagram.png new file mode 100644 index 000000000000..0eee6bb06b07 Binary files /dev/null and b/docs/images/OpenSequenceDiagram.png differ diff --git a/docs/images/PrevSequenceDiagram.png b/docs/images/PrevSequenceDiagram.png new file mode 100644 index 000000000000..a4b950600540 Binary files /dev/null and b/docs/images/PrevSequenceDiagram.png differ diff --git a/docs/images/SaveCommandSequenceDiagram.png b/docs/images/SaveCommandSequenceDiagram.png new file mode 100644 index 000000000000..8c1fa8d1b5c7 Binary files /dev/null and b/docs/images/SaveCommandSequenceDiagram.png differ diff --git a/docs/images/SelectCommand1.png b/docs/images/SelectCommand1.png new file mode 100644 index 000000000000..b4706db5e111 Binary files /dev/null and b/docs/images/SelectCommand1.png differ diff --git a/docs/images/SelectCommand2.png b/docs/images/SelectCommand2.png new file mode 100644 index 000000000000..0d7e6a23b167 Binary files /dev/null and b/docs/images/SelectCommand2.png differ diff --git a/docs/images/SelectCommand3.png b/docs/images/SelectCommand3.png new file mode 100644 index 000000000000..481ed51681de Binary files /dev/null and b/docs/images/SelectCommand3.png differ diff --git a/docs/images/SelectSequenceDiagram.png b/docs/images/SelectSequenceDiagram.png new file mode 100644 index 000000000000..d5febe57ee61 Binary files /dev/null and b/docs/images/SelectSequenceDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 7a4cd2700cbf..b56f1139c966 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/TransformationActivityDiagram.png b/docs/images/TransformationActivityDiagram.png new file mode 100644 index 000000000000..501b8ae531f2 Binary files /dev/null and b/docs/images/TransformationActivityDiagram.png differ diff --git a/docs/images/Ui-Clue.png b/docs/images/Ui-Clue.png new file mode 100644 index 000000000000..2db9a64f77d2 Binary files /dev/null and b/docs/images/Ui-Clue.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..cca52102fd50 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram_Piconso.png b/docs/images/UiClassDiagram_Piconso.png new file mode 100644 index 000000000000..03b81c09b519 Binary files /dev/null and b/docs/images/UiClassDiagram_Piconso.png differ diff --git a/docs/images/UiComponentClassDiagram.png b/docs/images/UiComponentClassDiagram.png new file mode 100644 index 000000000000..0fb20a63a723 Binary files /dev/null and b/docs/images/UiComponentClassDiagram.png differ diff --git a/docs/images/UndoRedoActivityDiagram.png b/docs/images/UndoRedoActivityDiagram.png deleted file mode 100644 index 55e4138cc64f..000000000000 Binary files a/docs/images/UndoRedoActivityDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoExecuteUndoStateListDiagram.png b/docs/images/UndoRedoExecuteUndoStateListDiagram.png deleted file mode 100644 index 29c365d6b4a1..000000000000 Binary files a/docs/images/UndoRedoExecuteUndoStateListDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoNewCommand1StateListDiagram.png b/docs/images/UndoRedoNewCommand1StateListDiagram.png deleted file mode 100644 index 76e661d62027..000000000000 Binary files a/docs/images/UndoRedoNewCommand1StateListDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoNewCommand2StateListDiagram.png b/docs/images/UndoRedoNewCommand2StateListDiagram.png deleted file mode 100644 index adcb9aeadc51..000000000000 Binary files a/docs/images/UndoRedoNewCommand2StateListDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoNewCommand3StateListDiagram.png b/docs/images/UndoRedoNewCommand3StateListDiagram.png deleted file mode 100644 index aac9c5fe05db..000000000000 Binary files a/docs/images/UndoRedoNewCommand3StateListDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoNewCommand4StateListDiagram.png b/docs/images/UndoRedoNewCommand4StateListDiagram.png deleted file mode 100644 index 66a0a3b5f323..000000000000 Binary files a/docs/images/UndoRedoNewCommand4StateListDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoSequenceDiagram.png b/docs/images/UndoRedoSequenceDiagram.png deleted file mode 100644 index 5c9d5936f098..000000000000 Binary files a/docs/images/UndoRedoSequenceDiagram.png and /dev/null differ diff --git a/docs/images/UndoRedoStartingStateListDiagram.png b/docs/images/UndoRedoStartingStateListDiagram.png deleted file mode 100644 index 002f3e2bbf79..000000000000 Binary files a/docs/images/UndoRedoStartingStateListDiagram.png and /dev/null differ diff --git a/docs/images/base.png b/docs/images/base.png new file mode 100644 index 000000000000..85249b07d559 Binary files /dev/null and b/docs/images/base.png differ diff --git a/docs/images/before-after.png b/docs/images/before-after.png new file mode 100644 index 000000000000..0d7b0ed39adb Binary files /dev/null and b/docs/images/before-after.png differ diff --git a/docs/images/benedictcss.png b/docs/images/benedictcss.png new file mode 100644 index 000000000000..954b3ef84559 Binary files /dev/null and b/docs/images/benedictcss.png differ diff --git a/docs/images/chivent.png b/docs/images/chivent.png new file mode 100644 index 000000000000..5d838b336696 Binary files /dev/null and b/docs/images/chivent.png differ diff --git a/docs/images/coords.png b/docs/images/coords.png new file mode 100644 index 000000000000..c22dd62aab89 Binary files /dev/null and b/docs/images/coords.png differ diff --git a/docs/images/exampleStep0.png b/docs/images/exampleStep0.png new file mode 100644 index 000000000000..a697a1f744f0 Binary files /dev/null and b/docs/images/exampleStep0.png differ diff --git a/docs/images/exampleStep1.png b/docs/images/exampleStep1.png new file mode 100644 index 000000000000..91a21502da6d Binary files /dev/null and b/docs/images/exampleStep1.png differ diff --git a/docs/images/exampleStep2.png b/docs/images/exampleStep2.png new file mode 100644 index 000000000000..b83f3dcf8378 Binary files /dev/null and b/docs/images/exampleStep2.png differ diff --git a/docs/images/exampleStep3.png b/docs/images/exampleStep3.png new file mode 100644 index 000000000000..fe263e0df6fc Binary files /dev/null and b/docs/images/exampleStep3.png differ diff --git a/docs/images/exampleStep4.png b/docs/images/exampleStep4.png new file mode 100644 index 000000000000..fb441ea517b8 Binary files /dev/null and b/docs/images/exampleStep4.png differ diff --git a/docs/images/exampleStep5.png b/docs/images/exampleStep5.png new file mode 100644 index 000000000000..9c185ce36f04 Binary files /dev/null and b/docs/images/exampleStep5.png differ diff --git a/docs/images/historyPanel.png b/docs/images/historyPanel.png new file mode 100644 index 000000000000..787f0307e676 Binary files /dev/null and b/docs/images/historyPanel.png differ diff --git a/docs/images/ihwk1996.png b/docs/images/ihwk1996.png new file mode 100644 index 000000000000..bfc3497a672a Binary files /dev/null and b/docs/images/ihwk1996.png differ diff --git a/docs/images/j-lum.png b/docs/images/j-lum.png new file mode 100644 index 000000000000..3fc07da96313 Binary files /dev/null and b/docs/images/j-lum.png differ diff --git a/docs/images/lancelotwillow.png b/docs/images/lancelotwillow.png new file mode 100644 index 000000000000..977938435d01 Binary files /dev/null and b/docs/images/lancelotwillow.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/step 1.png b/docs/images/step 1.png new file mode 100644 index 000000000000..8f8dc82397f7 Binary files /dev/null and b/docs/images/step 1.png differ diff --git a/docs/images/step 10.png b/docs/images/step 10.png new file mode 100644 index 000000000000..03a0a3f0a063 Binary files /dev/null and b/docs/images/step 10.png differ diff --git a/docs/images/step 2.png b/docs/images/step 2.png new file mode 100644 index 000000000000..fd2bb96fe632 Binary files /dev/null and b/docs/images/step 2.png differ diff --git a/docs/images/step 3.png b/docs/images/step 3.png new file mode 100644 index 000000000000..fb800e89ff40 Binary files /dev/null and b/docs/images/step 3.png differ diff --git a/docs/images/step 4.png b/docs/images/step 4.png new file mode 100644 index 000000000000..9ce7e5449a3e Binary files /dev/null and b/docs/images/step 4.png differ diff --git a/docs/images/step 5.png b/docs/images/step 5.png new file mode 100644 index 000000000000..247a05681d45 Binary files /dev/null and b/docs/images/step 5.png differ diff --git a/docs/images/step 6.png b/docs/images/step 6.png new file mode 100644 index 000000000000..874db3bfbbe0 Binary files /dev/null and b/docs/images/step 6.png differ diff --git a/docs/images/step 7.png b/docs/images/step 7.png new file mode 100644 index 000000000000..f6671b46a85d Binary files /dev/null and b/docs/images/step 7.png differ diff --git a/docs/images/step 8.png b/docs/images/step 8.png new file mode 100644 index 000000000000..9109a0bdf276 Binary files /dev/null and b/docs/images/step 8.png differ diff --git a/docs/images/step 9.png b/docs/images/step 9.png new file mode 100644 index 000000000000..0b65144d92f6 Binary files /dev/null and b/docs/images/step 9.png differ diff --git a/docs/images/tutorial/base.png b/docs/images/tutorial/base.png new file mode 100644 index 000000000000..85249b07d559 Binary files /dev/null and b/docs/images/tutorial/base.png differ diff --git a/docs/images/tutorial/before-after.png b/docs/images/tutorial/before-after.png new file mode 100644 index 000000000000..0d7b0ed39adb Binary files /dev/null and b/docs/images/tutorial/before-after.png differ diff --git a/docs/images/tutorial/step 1.png b/docs/images/tutorial/step 1.png new file mode 100644 index 000000000000..8f8dc82397f7 Binary files /dev/null and b/docs/images/tutorial/step 1.png differ diff --git a/docs/images/tutorial/step 10.png b/docs/images/tutorial/step 10.png new file mode 100644 index 000000000000..03a0a3f0a063 Binary files /dev/null and b/docs/images/tutorial/step 10.png differ diff --git a/docs/images/tutorial/step 2.png b/docs/images/tutorial/step 2.png new file mode 100644 index 000000000000..fd2bb96fe632 Binary files /dev/null and b/docs/images/tutorial/step 2.png differ diff --git a/docs/images/tutorial/step 3.png b/docs/images/tutorial/step 3.png new file mode 100644 index 000000000000..fb800e89ff40 Binary files /dev/null and b/docs/images/tutorial/step 3.png differ diff --git a/docs/images/tutorial/step 4.png b/docs/images/tutorial/step 4.png new file mode 100644 index 000000000000..9ce7e5449a3e Binary files /dev/null and b/docs/images/tutorial/step 4.png differ diff --git a/docs/images/tutorial/step 5.png b/docs/images/tutorial/step 5.png new file mode 100644 index 000000000000..247a05681d45 Binary files /dev/null and b/docs/images/tutorial/step 5.png differ diff --git a/docs/images/tutorial/step 6.png b/docs/images/tutorial/step 6.png new file mode 100644 index 000000000000..874db3bfbbe0 Binary files /dev/null and b/docs/images/tutorial/step 6.png differ diff --git a/docs/images/tutorial/step 7.png b/docs/images/tutorial/step 7.png new file mode 100644 index 000000000000..f6671b46a85d Binary files /dev/null and b/docs/images/tutorial/step 7.png differ diff --git a/docs/images/tutorial/step 8.png b/docs/images/tutorial/step 8.png new file mode 100644 index 000000000000..9109a0bdf276 Binary files /dev/null and b/docs/images/tutorial/step 8.png differ diff --git a/docs/images/tutorial/step 9.png b/docs/images/tutorial/step 9.png new file mode 100644 index 000000000000..0b65144d92f6 Binary files /dev/null and b/docs/images/tutorial/step 9.png differ diff --git a/docs/images/tutorial/tutorial.zip b/docs/images/tutorial/tutorial.zip new file mode 100644 index 000000000000..3dc645119b23 Binary files /dev/null and b/docs/images/tutorial/tutorial.zip differ diff --git a/docs/images/undoRedo1.png b/docs/images/undoRedo1.png new file mode 100644 index 000000000000..1e0b595f136a Binary files /dev/null and b/docs/images/undoRedo1.png differ diff --git a/docs/images/undoRedo2.png b/docs/images/undoRedo2.png new file mode 100644 index 000000000000..aa03c84264eb Binary files /dev/null and b/docs/images/undoRedo2.png differ diff --git a/docs/images/undoRedo3.png b/docs/images/undoRedo3.png new file mode 100644 index 000000000000..d7ad88ba0cca Binary files /dev/null and b/docs/images/undoRedo3.png differ diff --git a/docs/images/undoRedo4.png b/docs/images/undoRedo4.png new file mode 100644 index 000000000000..9a38b572a0d7 Binary files /dev/null and b/docs/images/undoRedo4.png differ diff --git a/docs/images/undoRedo5.png b/docs/images/undoRedo5.png new file mode 100644 index 000000000000..2b661877adc2 Binary files /dev/null and b/docs/images/undoRedo5.png differ diff --git a/docs/images/undoRedoActivityDiagram.png b/docs/images/undoRedoActivityDiagram.png new file mode 100644 index 000000000000..3228235acda4 Binary files /dev/null and b/docs/images/undoRedoActivityDiagram.png differ diff --git a/docs/images/undoRedoActivityDiagram2.png b/docs/images/undoRedoActivityDiagram2.png new file mode 100644 index 000000000000..3c6b808dcf79 Binary files /dev/null and b/docs/images/undoRedoActivityDiagram2.png differ diff --git a/docs/images/undoRedoActivityDiagram3.png b/docs/images/undoRedoActivityDiagram3.png new file mode 100644 index 000000000000..2d38881ec055 Binary files /dev/null and b/docs/images/undoRedoActivityDiagram3.png differ diff --git a/docs/images/undoRedoSequenceDiagram.png b/docs/images/undoRedoSequenceDiagram.png new file mode 100644 index 000000000000..edee170763d7 Binary files /dev/null and b/docs/images/undoRedoSequenceDiagram.png differ diff --git a/docs/images/uploadExplanation.png b/docs/images/uploadExplanation.png new file mode 100644 index 000000000000..3f919140dc32 Binary files /dev/null and b/docs/images/uploadExplanation.png differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/stylesheets/asciidoctor.css b/docs/stylesheets/asciidoctor.css index 36590bf346cd..5bd5f774cc22 100644 --- a/docs/stylesheets/asciidoctor.css +++ b/docs/stylesheets/asciidoctor.css @@ -378,7 +378,6 @@ p{margin-bottom:1.25rem} *{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important} a{color:inherit!important;text-decoration:underline!important} a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important} -a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em} abbr[title]:after{content:" (" attr(title) ")"} pre,blockquote,tr,img,object,svg{page-break-inside:avoid} thead{display:table-header-group} diff --git a/docs/team/benedictcss.adoc b/docs/team/benedictcss.adoc new file mode 100644 index 000000000000..50742bed32b2 --- /dev/null +++ b/docs/team/benedictcss.adoc @@ -0,0 +1,98 @@ += Benedict - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Piconso + +--- + +== Overview + +This portfolio seeks to document my contributions to our team’s project, Piconso. Piconso is an CLI +(Command Line Interface) Image Editor designed and developed for quick and efficient image-editing on your Desktop. +All traditional image-editing features can be performed solely through text commands, removing the hassle of +working with a mouse. Piconso does not only benefit the power users but also serves as a learning platform for new users +with its simple command line tools. + +Below is a list of main features of our application: + +* Several traditional image-editing features such as rotate, blur etc. +* Capability to define own custom transformation set. +* Direct access to images on Google Photos (download and upload). + + +== Summary of contributions +|=== +|_This section summarises the implementations and contributions I've made for this project._ +|=== + +=== *Major Enhancement* -- Ability to traverse into different directories + +As majority of users have their images stored in different folders within their system drive, we decided to implement +a feature that would ease users' access to this images. Therefore, I have added the `cd` feature which allows +users to access these images without the need to store all their images to a specified folder for access. + +** Highlights: To improve user's performance, I have coupled the `cd` feature with the implementation of the `Tab` function +which would auto-completes the user input with an existing directory in the current folder. + +=== *Major Enhancement* -- Ability to open images for editing + +Before any user could start editing an image, they would first need to open the image for editing. As such, the `select` +feature in AB4 was refactored and modified to the `open` feature we have here on Piconso. This feature allows users into +open an image from the current batch to be displayed on the GUI before any edits are made. + +** Highlights: This enhancement requires in depth analysis of design alternative to ensure maximum efficiency in +retrieving images from the current directory. + +=== *Minor Enhancement* -- Ability to traverse between image batches + +As Piconso accesses only 10 images at any one time, the `next` and `prev` feature enables the user to access other +images within the same directory. This feature facilitate the ease of traversal of all the images within the current +directory. + +=== *Code contributed* + +Here are the code contributions that I have made. [https://github.com/CS2103-AY1819S1-T09-3/main/commits?author=benedictcss[All commits]] [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=benedictcss[Project Code Dashboard]] + +=== *Other contributions*: + +Below are other contributions that I have made for this project. + +==== Project Management: +* Created labels, milestones and issues for issue tracking on GitHub +* Participated in tagging issues and merging/reviewing PRs +* Set up appveyor build, coveralls and reposense config file on Github. + +==== Documentation: +* Added detailed implementation documentation for the directories command in Developer Guide, including diagrams: https://github.com/CS2103-AY1819S1-T09-3/main/pull/213[#213] +* Updated diagrams for Logic, Model and UI component. https://github.com/CS2103-AY1819S1-T09-3/main/pull/174[#174] + +==== Enhancement to existing features: +* Updated the icon of the application. https://github.com/CS2103-AY1819S1-T09-3/main/pull/111[#111] +* Wrote additional test for existing features. https://github.com/CS2103-AY1819S1-T09-3/main/pull/107[#107] + +==== Community: +* PRs reviewed: https://github.com/CS2103-AY1819S1-T09-3/main/pull/56[#56] + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the User Guide. +They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=dir] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. + They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=cd] + +include::../DeveloperGuide.adoc[tag=open] + +include::../DeveloperGuide.adoc[tag=nextprev] diff --git a/docs/team/chivent.adoc b/docs/team/chivent.adoc new file mode 100644 index 000000000000..e50629be7cb2 --- /dev/null +++ b/docs/team/chivent.adoc @@ -0,0 +1,104 @@ += Ong Kai Le - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Piconso + +--- + +== Overview + +Piconso is an CLI-based (Command Line Interface) image editor designed and developed for quick and efficient image-editing. +Many of today's top image editing softwares are often overloaded with features, and for those who just want to perform and learn basic image editing, the learning curve of these softwares is way too steep. + +.A view of Piconso +image::Ui.png[width:602] + +Sporting a simplified CLI interface, it is easy to navigate and interact with Piconso through basic text commands. The product caters especially towards individuals familiar with CLI but at the same time, aims to be easy for new users such as photographers to pick up. + +The application is Java-based and well documented, with its code quality ensured via a series of manual and unit tests. The following will detail more about my contributions to the development of Piconso. + +== Summary of contributions + +=== *Main enhancement*: Enabled connectivity to Google Photos + +Google Photos has become a popular platform that is used by many to safely store and share their images. As such, allowing connectivity to Google allows users to quickly retrieve their stored images on Google Photos for offline editing without having to leave Piconso. + +This enhancement provides users with the ability to: + +* Traverse Google Photos and filter images by album +* Download an image or an entire album +* Upload an image or an entire folder + +With this feature, the user can instantly download, edit and upload his edited photo for safe storage and quick sharing to other users. This especially adds value to users such as photographers or developers, who may have a large batch of images stored on the cloud that they would like to edit. + +* _Highlights_: This enhancement runs authentication asynchronously such that if a login request is not followed through, it leaves the main application unaffected. Additionally, the upload, download and list features all have multiple command variants for user convenience. +* _Credit_: Google Photos Java Client API and Google OAuth API have been used throughout Google related commands. Certain parts of the aspect that allows login to Google Photos was also based upon the https://github.com/google/java-photoslibrary/blob/master/sample/src/main/java/com/google/photos/library/sample/demos/AlbumDemo.java[example found here] + + +=== *Minor enhancements*: +*Implemented a ls command* + +Similar to `ls` used in consoles, the ls command I implemented allows users to view the content of the directory they are currently in. However, the `ls` implemented in this project only lists folders and images so as to prevent user confusion. + +*Created the initial User Interface (UI) and logic for HistoryListPanel* + +The HistoryListPanel (as seen on the upper left of the application) provides a way for users to keep track of the transformations they have performed thus far on the layer in a visual form. +The logic previously handled only transformations on one layer, but was later changed by the team after the implementation of layers to incorporate it. + +* _Highlights_: The list currently constantly highlights the last performed transformation, and selection on the panel has been disabled to prevent user confusion. + +*Created UI and logic for Film Reel* + +We came to a decision that viewing all images at once would be too confusing for the user, and thus we decided to show the image by _batches_ +such that a user can only view 10 images at once, and navigate via `next` and `preview` commands. + +The FilmReel (as seen on the lower left of the application) shows the currently viewed batch of images in the directory. It provides an easy way for users to preview their images and know the image index associated with it. + +//* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ +=== *Code contributed*: +Here are links to my contributed code: [https://github.com/CS2103-AY1819S1-T09-3/main/commits?author=chivent[All commits]] [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=chivent[Project Code Dashboard]] + +=== *Other contributions*: + +Aside from my main contributions, I have also participated in performing the following general tasks + +*Project management:* + +* Helped manage tagging of milestone code (v1.1-v1.2) +* Participated in creating and tagging issues, and merging/reviewing PRs + +*Enhancements to existing features:* + +* Updated the StatusFooterBar UI to display the current directory and google connectivity status (Pull requests https://github.com/CS2103-AY1819S1-T09-3/main/pull/80[#80]) +* Renamed "AddressBook" instances from base project to relate to our application instead. + +*Documentation:* + +* Added detailed implementation documentation for the Google-related features in the Developer Guide, including diagrams and images: https://github.com/CS2103-AY1819S1-T09-3/main/pull/131[#131], https://github.com/CS2103-AY1819S1-T09-3/main/pull/195[#195] +* Updated the User Guide for Google-related features: https://github.com/CS2103-AY1819S1-T09-3/main/pull/103[#103], https://github.com/CS2103-AY1819S1-T09-3/main/pull/131[#131] +* Performed general updates in DeveloperGuide, AboutUs and README documents https://github.com/CS2103-AY1819S1-T09-3/main/pull/200[#200], https://github.com/CS2103-AY1819S1-T09-3/main/pull/203[#203]. + +*Community:* + +* PRs merged: https://github.com/CS2103-AY1819S1-T09-3/main/pull/108[#108], https://github.com/CS2103-AY1819S1-T09-3/main/pull/110[#110] +* PRs reviewed: https://github.com/CS2103-AY1819S1-T09-3/main/pull/44[#44], https://github.com/CS2103-AY1819S1-T09-3/main/pull/37[#37] + +== Contributions to the User Guide + +|=== +|_Given below are sections I have contributed to the User Guide. +They showcase my ability to write user-friendly documentation._ +|=== + +include::../UserGuide.adoc[tag=google] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. +They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=google] diff --git a/docs/team/ihwk1996.adoc b/docs/team/ihwk1996.adoc new file mode 100644 index 000000000000..527fd7f20f0c --- /dev/null +++ b/docs/team/ihwk1996.adoc @@ -0,0 +1,72 @@ += Ivan Ho - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== Introduction + +image::ihwk1996.png[width="200", align="left"] +{empty}[http://github.com/ihwk1996[github]] [https://linkedin.com/in/ivanhowengkwong[linkedin]] + +Hello there! I am Ivan, a Year 2 Computer Science student in NUS. This portfolio documents the contributions I have made to several projects. These projects have exposed me to new technologies and helped me to develop relevant skills en route to being a Software Engineer. + +== PROJECT: Piconso + +== Overview + +Piconso is a robust yet simple image-editing tool. Catered towards power users, Piconso users interact through the Command Line Interface (CLI), while providing visual feedback through Graphical User Interface (GUI). +Unlike other heavy image editors, Piconso has a very gentle learning curve as it abstracts out clutter by providing the more essential features (complex editing can still be done with special commands). The features and implementations are well documented in guides for users and developers respectively. +In addition, Piconso is well maintained with high reliability and code quality as it is covered by rigorous tests and checks. + +.A view of Piconso +image::../images/Ui.png[width="602"] + +== Role + +My role was to facilitate the history of transformations of images within each layer. As the undo and redo feature is done using memoization, images need to be cached after each transformation. I constructed the base class to manage the caching and retrieval of images, then linked it to the Model and Storage components. In addition, I wrote rigorous tests and test utilities to ensure high reliability and simpler maintenance. + +== Summary of contributions + +* *Major enhancement*: added *the ability to undo/redo previous transformations* +** What it does: allows the user to undo previous transformations one at a time. Preceding undone transformations can be reversed by using the redo command. +** Justification: This feature improves the product significantly because a user can make mistakes by applying incorrect transformations and the app should provide a convenient way to rectify them. +** Highlights: This enhancement uses memoization to ensure undo/redo commands are almost instant. Since transformations to images take significant time (especially on large images) and that users typically want to quickly inspect the before and after of their transformation, it is necessary for undo and redo to be fast. As such, after each transformation, the resulting transformed image is cached into a temporary folder. Undo and redo operations will shift a pointer leftwards or rightwards and read the selected image, instead of reapplying or reversing a transformation process which is time consuming. + +* *Minor enhancement*: added *the ability to undo/redo all transformations at once* +** What it does: allows the user to undo all previous transformations swiftly with a single `undo-all` command. Likewise, all preceding undone transformations can be redone with a single `redo-all` command. +** Justification: This feature provides the user with an extra layer of convenience. At times, the user might feel that he wants to revert back to the original state but has applied many transformations. It would be tedious to apply the undo command one by one, hence having command to undo all transformations would be handy. Similarly, if he feels that undoing all commands is a mistake, he can quickly revert back all transformations with the `redo-all` command. +** Highlights: Similar the the undo/redo feature, undo-all/redo-all commands are also almost instant with the help of memoization. + + +* *Code contributed*: [https://github.com/CS2103-AY1819S1-T09-3/main/commits?author=ihwk1996[All commits]] [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=ihwk1996[Project Code Dashboard]] + + +* *Other contributions*: + +** Project management: +*** Set up team organization and repository on GitHub +** Enhancements to existing features: +*** Wrote tests for undo/redo feature including various test utility classes to assist other team members in their testing (Pull requests https://github.com/CS2103-AY1819S1-T09-3/main/pull/105[#105], https://github.com/CS2103-AY1819S1-T09-3/main/pull/110[#110]) +** Documentation: +*** Added detailed implementation documentation for the undo/redo feature in Developer Guide, including diagrams (Pull requests https://github.com/CS2103-AY1819S1-T09-3/main/pull/56[#56], https://github.com/CS2103-AY1819S1-T09-3/main/pull/60[#60]) +** Community: +*** Reviewed and gave feedback to team members. PRs reviewed: https://github.com/CS2103-AY1819S1-T09-3/main/pull/37[#37], https://github.com/CS2103-AY1819S1-T09-3/main/pull/55[#55], https://github.com/CS2103-AY1819S1-T09-3/main/pull/57[#57], https://github.com/CS2103-AY1819S1-T09-3/main/pull/61[#61] +** Tools: +*** Set up Travis(Continuous Integration) to perform automated testing of test cases and auto-publishing of documentation + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users. (Since undo/redo are simple to understand, there is not much to document here. More details are in the implementation of undo/redo in the Developer Guide)_ +|=== + +include::../UserGuide.adoc[tag=undoredo] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=undoredo] diff --git a/docs/team/j-lum.adoc b/docs/team/j-lum.adoc new file mode 100644 index 000000000000..2a960310cb92 --- /dev/null +++ b/docs/team/j-lum.adoc @@ -0,0 +1,98 @@ += Jeffry - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== Project: Piconso + +== Overview + +Piconso is a powerful general-purpose image editing tool. +The user interacts with it through a flexible command-line interface and receives instant visual feedback through the Graphical User Interface (GUI) written in JavaFX. +The application is mainly written in Java and spans a considerable 10k Lines of Code (LoC). +The codebase is well-maintained with a rigorous system of checks and tests in place to ensure that code quality is consistently high. +A comprehensive set of guides are also provided to ensure a smooth on-boarding process for both users and contributors alike. + +== Summary of contributions +As the point man of the group, I led the group through discussions from idea generation to delegation of work. +I placed extra emphasis on engineering optimal solutions to real pain points. +As a result, my team was able to morph the given codebase from a trivial application into a polished product. + +I put my considerable experience to use by being available at all times to help out with various tasks like design considerations and debugging. I believe that my contribution has improved the unlying engineering of the codebase which in turn sped up the application manyfold. + +My major contributions are as follows: + +**Designed and implemented the unlying data structures and models for the manipulation of canvases and layers.** + +This allowed other team members to maintain a clear separation of concerns and reduce dependencies which in turn made the code base less prone to breaking changes. This is a major change that affects virtually all of the code base and future developers looking to add or improve Piconso will have to build upon my models. + +In fact, I have already extended my models to provide end-users with a hassle-free and easily understandable way to navigate more intricate image editing features such as layer ordering and canvas size. This improves the user experience significantly as these features are expected of any modern image editing tool. It also reduces the friction of users migrating to Piconso from other competitors. + + +**Added a sub-command targeted at tech-savvy users : `raw`.** + +In line with our goal of being a powerful and flexible tool, I have added a feature that allows users to tap into the full power of Piconso. This is a notable departure from the other commands in the application which can be fairly restrictive but easy to learn. The raw command allows users to quickly experiment and iterate upon their image editing process without excessive hand-holding from the application. + +By giving advanced users the freedom to do as they please, I have brought Piconso closer in terms of feature parity as compared to commercial heavy-weights like Photoshop. + +Developers will also save time on manually implementing obscure functionalities that the majority of the end-users will likely never use. This results in a more streamlined code base unlike Photoshop which clocks in at over 10 million LoCs. + +Besides that, I have also made multiple smaller contributions which significantly improves the user experience. + +**Implemented the ImagePanel** + +The ImagePanel is at the heart of Piconso, displaying the output of Piconso to the user visually. It was carefully engineered with reuse in mind and its usage painstakingly documented for future developers. + +**Improved upon the HistoryListPanel** + +Building upon the work of other teammates, I have improved the HistoryListPanel to reflect past operations on across multiple layers. Now users can quickly see what operations they have performed on each layer. + +**Implemented the LayerPanel** + +Similar to the HistoryListPanel, the LayerPanel displays information that that users might have trouble keeping track of in a clean and easily grokkable fashion. Users can clearly see which layer they are operating on as well as the order of layers. + +**Executed upon the change in layout** + +I rearranged the layout of the user interface components from the initial code base to one that was designed by the team to be more user-friendly. + +Here is the code that I have written for this product: [https://github.com/CS2103-AY1819S1-T09-3/main/commits?author=j-lum[All commits]] [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=j-lum[Dashboard]] + +**Other contributions** + +** Project management: + +I commented on critical pull requests. +I also extensively tested and reported bugs along with a comprehensive reproduction reports: https://github.com/CS2103-AY1819S1-W14-4/main/issues/170[#170] https://github.com/CS2103-AY1819S1-W14-4/main/issues/175[#175] + +** Documentation: + +I wrote a tutorial to walk users through the on-boarding process with a sample project that demonstrates most of the key features of our application. This light-hearted tutorial also serves to show that our application is relevant and solves problems that the reader can relate to. + +I also contributed multiple new illustrations to the developer's guide to help future contributors visualise what each command does. +The illustrations are included in my contributions below. + + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. +They showcase my ability to write comprehensive but simple documentation that addresses the needs of users._ +|=== + +include::../UserGuide.adoc[tag=getting_started1] + +The tutorial is truncated to account for the page limit. + +=== User guide command documentation +--- +include::../UserGuide.adoc[tag=canvas] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=layer] + diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index 453c2152ab9d..000000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 4 - ---- - -== Overview - -AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -== Summary of contributions - -* *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. -** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. -** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=undoredo] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/lancelotwillow.adoc b/docs/team/lancelotwillow.adoc new file mode 100644 index 000000000000..407d3e520921 --- /dev/null +++ b/docs/team/lancelotwillow.adoc @@ -0,0 +1,70 @@ += ZHANG TIANYANG - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== Project: Piconso + +== Overview + +Piconso - The Piconso is a command line image processing tool, supported by ImageMagick, and is built to handle all the image processing in a light weight and efficient way. Users are able to use command line to perform operations, and visualize the result in GUI which is powered by JavaFx. + +Unlink other image processing tools, without a long term learning process, piconso aims to privides simple, strarightforward, and user-friendly features. Moreover, piconso can be mouse free, which woould be more confortable for designers and developers. + +== Summary of contributions + +* *Major morphing*: +** *Enable the use of Imagemagick* +*** What it does: It determine the current operating system, prepare the zipped pacakge of ImageMagick, unzipp and prepare the ImageMagick package. +*** Justification:This is important as Imagemagick is the core part behind the app, allowing the editing of the images. + +** *The ability to apply the transformation to image* +*** What it does: It parses the arguments specified by user, validate the transforamtion and apply the transformation to images. +*** Justification: This feature improves the product significantly, because apply the transformations to the image is the main feature and purpose of piconso. +*** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation was challenging as it required changes to existing commands, preparing packages, and touching the external package and excutbale files. +*** Credits: _{The processing of images is done by ImageMagic by calling the executable file in the processbuilder}_ + +** *The ability to customise transformation basing on the transformations currectly* +*** What it does: This feature allows user to design and apply their own transformation. +*** Justification: This feature improves the product significantly as well, because when doing image editing, multiple transformations are used all the time. With this feature, the image editing will be much more efficient. +*** Highlights: This enhancement also affects existing commands and commands to be added in future. The implementation was challenging as it need handle file creating, writing and accessing. + +* *Minor enhancement*: +** *Add a save command that allows the user to save the transformation wanted.* +*** What it does: It allows users to save the changes to the current image to the disk. + +* *Code contributed*: [https://github.com/CS2103-AY1819S1-T09-3/main/commits?author=lancelotwillow[All commits]] [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=lancelotwillow[Project Code Dashboard]] + +* *Other contributions*: + +** Project management: +*** Managed releases jar file v1.3, v1.3.1 on GitHub +*** Participating merging PRs, creating, handling issues. +** Documentation: +*** Add info to the User Guide and Developer Guide about the apply, create, save commands. +https://github.com/CS2103-AY1819S1-T09-3/main/pull/125[#125] +*** Uploading User Guide for transforamtion and save features. +https://github.com/CS2103-AY1819S1-T09-3/main/pull/177[#177] +** Community: +*** PRs reviewed (with non-trivial review comments, examples): https://github.com/CS2103-AY1819S1-T09-3/main/pull/195[#195], https://github.com/CS2103-AY1819S1-T09-3/main/pull/128[#128] +*** Contributed to forum discussions (examples): https://github.com/nus-cs2103-AY1819S1/forum/issues/131[#131], https://github.com/nus-cs2103-AY1819S1/forum/issues/167[#167] +*** Create demo usage of the command, for testing of other team mates.(Example command, has been removed now) +** Tools: +*** Integrated a third party library (ImageMagic) to the project (https://github.com[#42]) + +== Contributions to the User Guide +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== +include::../UserGuide.adoc[tag=apply] +include::../UserGuide.adoc[tag=create] +include::../UserGuide.adoc[tag=save] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== +include::../DeveloperGuide.adoc[tag=transformation] +include::../DeveloperGuide.adoc[tag=save] + diff --git a/docs/templates/_header.html.slim b/docs/templates/_header.html.slim index 1995d26a1615..8d840758c992 100644 --- a/docs/templates/_header.html.slim +++ b/docs/templates/_header.html.slim @@ -1,26 +1,4 @@ / NOTE: You must restart the gradle daemon after modifying any template file for the changes to take effect. -- if !(attr? 'no-site-header') && (attr? 'site-seedu') - #seedu-header - nav.navbar.navbar-lg.navbar-light.bg-lighter - .container - a.navbar-brand href='https://se-edu.github.io/' - img src=(site_url 'images/SeEduLogo.png') alt='SE-EDU' - ul.navbar-nav - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level1' AB-1 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level2' AB-2 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level3' AB-3 - li.nav-item - a.nav-link.active href=(site_url 'index.html') AB-4 - li.nav-item - a.nav-link href='https://se-edu.github.io/collate' Collate - li.nav-item - a.nav-link href='https://se-edu.github.io/se-book' Book - li.nav-item - a.nav-link href='https://se-edu.github.io/learningresources' Resources - - if !(attr? 'no-site-header') #site-header nav.navbar.navbar-light.bg-light diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2d80b69a7665..a68fd1680d5b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Oct 27 19:17:00 SGT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip diff --git a/mac.zip b/mac.zip new file mode 100644 index 000000000000..ebed10ca0cff Binary files /dev/null and b/mac.zip differ diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index ecdd043a4f81..89a16103ad0e 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -1,7 +1,11 @@ package seedu.address; +import static seedu.address.model.google.PhotosLibraryClientFactory.BLOCKER; +import static seedu.address.model.google.PhotosLibraryClientFactory.TEST_FILE; + import java.io.IOException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import java.util.logging.Logger; @@ -17,30 +21,31 @@ import seedu.address.commons.events.ui.ExitAppRequestEvent; import seedu.address.commons.exceptions.DataConversionException; import seedu.address.commons.util.ConfigUtil; +import seedu.address.commons.util.FileUtil; +import seedu.address.commons.util.ImageMagickUtil; import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; +import seedu.address.model.google.PhotosLibraryClientFactory; import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; -import seedu.address.storage.XmlAddressBookStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; +// Travis test commit + /** * The main entry point to the application. */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); + public static final Path MAIN_PATH = Paths.get("").toAbsolutePath(); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -54,54 +59,30 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing Piconso ]==========================="); super.init(); - + FileUtil.deleteIfAvaliable(TEST_FILE); AppParameters appParameters = AppParameters.parse(getParameters()); config = initConfig(appParameters.getConfigPath()); - UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new XmlAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + ImageMagickUtil.copyOutside(userPrefs, System.getProperty("os.name").toLowerCase()); + storage = new StorageManager(userPrefsStorage); initLogging(config); - model = initModelManager(storage, userPrefs); + model = new ModelManager(userPrefs, false); logic = new LogicManager(model); - ui = new UiManager(logic, config, userPrefs); + ui = new UiManager(logic, config, userPrefs, model.getUserLoggedIn()); - initEventsCenter(); - } - /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. - */ - private Model initModelManager(Storage storage, UserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; - try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); - } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); - } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); - } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); - } + initEventsCenter(); - return new ModelManager(initialData, userPrefs); } - private void initLogging(Config config) { + protected void initLogging(Config config) { LogsCenter.init(config); } @@ -159,12 +140,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { + "Using default user prefs"); initializedPrefs = new UserPrefs(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting from default"); initializedPrefs = new UserPrefs(); } //Update prefs file in case it was missing to begin with or there are new/unused fields try { + initializedPrefs.updateUserPrefs(Paths.get(System.getProperty("user.home"))); storage.saveUserPrefs(initializedPrefs); } catch (IOException e) { logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); @@ -173,25 +155,33 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { return initializedPrefs; } - private void initEventsCenter() { + protected void initEventsCenter() { EventsCenter.getInstance().registerHandler(this); } @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting Piconso " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping Piconso ] ============================="); ui.stop(); try { storage.saveUserPrefs(userPrefs); + storage.clearCache(); } catch (IOException e) { logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); } + + FileUtil.deleteIfAvaliable(TEST_FILE); + + if (BLOCKER.exists()) { + PhotosLibraryClientFactory.logoutUserIfPossible(); + } + Platform.exit(); System.exit(0); } diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java index e978d621e086..f0fef9a78a5e 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/address/commons/core/Config.java @@ -13,7 +13,7 @@ public class Config { public static final Path DEFAULT_CONFIG_FILE = Paths.get("config.json"); // Config values customizable through config file - private String appTitle = "Address App"; + private String appTitle = "Piconso"; private Level logLevel = Level.INFO; private Path userPrefsFilePath = Paths.get("preferences.json"); diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/address/commons/core/GuiSettings.java index ed578f1a9bb6..6ef15dc26075 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/address/commons/core/GuiSettings.java @@ -9,8 +9,8 @@ */ public class GuiSettings implements Serializable { - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_HEIGHT = 1280; + private static final double DEFAULT_WIDTH = 980; private Double windowWidth; private Double windowHeight; diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 5316a1d87d3e..a77d65fe571c 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -20,7 +20,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "piconso.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..d873c7bf20e9 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -1,5 +1,20 @@ package seedu.address.commons.core; +import seedu.address.logic.commands.CreateApplyCommand; +import seedu.address.logic.commands.SaveCommand; +import seedu.address.logic.commands.canvas.CanvasAutoResizeCommand; +import seedu.address.logic.commands.canvas.CanvasBgcolorCommand; +import seedu.address.logic.commands.canvas.CanvasSizeCommand; +import seedu.address.logic.commands.google.GoogleDlCommand; +import seedu.address.logic.commands.google.GoogleLsCommand; +import seedu.address.logic.commands.google.GoogleUploadCommand; +import seedu.address.logic.commands.layer.LayerAddCommand; +import seedu.address.logic.commands.layer.LayerDeleteCommand; +import seedu.address.logic.commands.layer.LayerPositionCommand; +import seedu.address.logic.commands.layer.LayerSelectCommand; +import seedu.address.logic.commands.layer.LayerSwapCommand; + + /** * Container for user visible messages. */ @@ -7,7 +22,54 @@ public class Messages { 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_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + /* Error message for traversing imageList */ + public static final String MESSAGE_INDEX_END_OF_IMAGE_LIST = "Exceeded number of images in current directory."; + public static final String MESSAGE_INDEX_EXCEED_MAX_BATCH_SIZE = "The index provided exceeds batch size."; + public static final String MESSAGE_TOTAL_IMAGES_IN_DIR = "Total number of images in directory: %d\n"; + public static final String MESSAGE_CURRENT_BATCH_IN_IMAGE_LIST = "Currently viewing images from %d to %d\n"; + public static final String MESSAGE_CURRENT_IMAGES_IN_BATCH = "Current number of images in batch: %d\n"; + public static final String MESSAGE_NO_MORE_NEXT_IMAGES = "No more images in the current directory."; + public static final String MESSAGE_NO_MORE_PREV_IMAGES = "Already at start of list."; + + /* Error messages for Google OAuth */ + public static final String MESSAGE_INVALID_ALBUM_REQUESTED = "The album name provided is invalid"; + public static final String MESSAGE_GOOGLE_INVALID_FORMAT = "Please enter a valid google command format."; + + public static final String MESSAGE_CONNECTION_FAILURE = "Error connecting to Google Photos, please try again."; + + public static final String MESSAGE_INVALID_IMAGE_REQUESTED = "The image name provided is invalid."; + public static final String ENTIRE_GOOGLE_MESSAGE = MESSAGE_GOOGLE_INVALID_FORMAT + "\n---------------------\n\n" + + GoogleLsCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + GoogleDlCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + GoogleUploadCommand.MESSAGE_USAGE; + + /* Error messages ls. */ + public static final String MESSAGE_INVALID_FILE_DIR = "File or directory invalid."; + public static final String MESSAGE_EMPTY_DIR = "No images or folders to display!"; + + /* Error messages for layer and canvas */ + public static final String MESSAGE_LAYER_INVALID_FORMAT = "Please enter a valid layer command."; + public static final String ENTIRE_LAYER_MESSAGE = MESSAGE_LAYER_INVALID_FORMAT + "\n------------------------\n\n" + + LayerAddCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + LayerDeleteCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + LayerPositionCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + LayerSelectCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + LayerSwapCommand.MESSAGE_USAGE + "\n------------------------\n\n"; + + public static final String MESSAGE_CANVAS_INVALID_FORMAT = "Please enter a valid canvas command."; + public static final String ENTIRE_CANVAS_MESSAGE = MESSAGE_CANVAS_INVALID_FORMAT + "\n------------------------\n\n" + + CanvasAutoResizeCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + CanvasBgcolorCommand.MESSAGE_USAGE + "\n------------------------\n\n" + + CanvasSizeCommand.MESSAGE_USAGE; + + /* Error messages for create convert command */ + public static final String MESSAGE_INVALID_OPERATION_ARGUMENTS = "Some of the arguments entered may be invalid, " + + "please check the arguments of each transformation entered\n" + + "\nYou may refer to User Guide (type `help`) Section 4.8 for more details on argument values." + + "\n\n Standard Format: " + CreateApplyCommand.MESSAGE_USAGE; + + /* Error message for save command*/ + public static final String MESSAGE_DUPLICATED_IMAGE = "Image with the same name already exists in this directory!\n" + + SaveCommand.MESSAGE_USAGE; } diff --git a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java deleted file mode 100644 index b72ad4740e5a..000000000000 --- a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package seedu.address.commons.events.model; - -import seedu.address.commons.events.BaseEvent; -import seedu.address.model.ReadOnlyAddressBook; - -/** Indicates the AddressBook in the model has changed*/ -public class AddressBookChangedEvent extends BaseEvent { - - public final ReadOnlyAddressBook data; - - public AddressBookChangedEvent(ReadOnlyAddressBook data) { - this.data = data; - } - - @Override - public String toString() { - return "number of persons " + data.getPersonList().size(); - } -} diff --git a/src/main/java/seedu/address/commons/events/ui/ChangeDirectoryEvent.java b/src/main/java/seedu/address/commons/events/ui/ChangeDirectoryEvent.java new file mode 100644 index 000000000000..3f5477d4e76c --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/ChangeDirectoryEvent.java @@ -0,0 +1,28 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.events.BaseEvent; + +//@@author chivent +/** + * An event that notifies StatusBarFooter on a change of directory. + */ +public class ChangeDirectoryEvent extends BaseEvent { + + public final String directory; + + /** + * Constructor for ChangeDirectoryEvent + * + * @param directory The current directory user is in + */ + public ChangeDirectoryEvent(String directory) { + this.directory = requireNonNull(directory); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/ChangeImageEvent.java b/src/main/java/seedu/address/commons/events/ui/ChangeImageEvent.java new file mode 100644 index 000000000000..84509354fee5 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/ChangeImageEvent.java @@ -0,0 +1,33 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import javafx.scene.image.Image; +import seedu.address.commons.events.BaseEvent; + +//@@author j-lum +/** + * A simple event that notifies a target ImagePanel to change its image. + */ + +public class ChangeImageEvent extends BaseEvent { + + public final Image image; + public final String target; + + /** + * Constructor for ChangeImageEvent + * + * @param image Image to replace + * @param target The name of the ImageView to target. + */ + public ChangeImageEvent(Image image, String target) { + this.image = requireNonNull(image); + this.target = requireNonNull(target); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/FilmReelSelectionChangeEvent.java b/src/main/java/seedu/address/commons/events/ui/FilmReelSelectionChangeEvent.java new file mode 100644 index 000000000000..c8bb133d38cc --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/FilmReelSelectionChangeEvent.java @@ -0,0 +1,36 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.events.BaseEvent; + +//@@author chivent +/** + * An event that notifies Film Reel ui regarding a selection change + */ +public class FilmReelSelectionChangeEvent extends BaseEvent { + + public final int index; + + /** + * Constructor for LoginStatusEvent + * + * @param index index of image chosen + */ + public FilmReelSelectionChangeEvent(int index) { + + requireNonNull(index); + + if (index >= 0) { + this.index = index; + } else { + this.index = -1; + } + + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/HistoryUpdateEvent.java b/src/main/java/seedu/address/commons/events/ui/HistoryUpdateEvent.java new file mode 100644 index 000000000000..dac1cc692d63 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/HistoryUpdateEvent.java @@ -0,0 +1,30 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; + +import seedu.address.commons.events.BaseEvent; + +//@@author j-lum +/** + * A simple event that notifies the history panel to refresh. + */ + +public class HistoryUpdateEvent extends BaseEvent { + + public final ArrayList list; + + /** + * Constructor for a HistoryUpdateEvent + * @param toChange - List of past operations to display + */ + public HistoryUpdateEvent(ArrayList toChange) { + this.list = requireNonNull(toChange); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java deleted file mode 100644 index a890f8b47350..000000000000 --- a/src/main/java/seedu/address/commons/events/ui/JumpToListRequestEvent.java +++ /dev/null @@ -1,22 +0,0 @@ -package seedu.address.commons.events.ui; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.events.BaseEvent; - -/** - * Indicates a request to jump to the list of persons - */ -public class JumpToListRequestEvent extends BaseEvent { - - public final int targetIndex; - - public JumpToListRequestEvent(Index targetIndex) { - this.targetIndex = targetIndex.getZeroBased(); - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - -} diff --git a/src/main/java/seedu/address/commons/events/ui/LayerUpdateEvent.java b/src/main/java/seedu/address/commons/events/ui/LayerUpdateEvent.java new file mode 100644 index 000000000000..117a0327a6de --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/LayerUpdateEvent.java @@ -0,0 +1,34 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; + +//@@author j-lum +/** + * A simple event that notifies the layer list to refresh. + */ + +public class LayerUpdateEvent extends BaseEvent { + + public final ArrayList list; + public final Index current; + + /** + * Constructor for a LayerUpdateEvent + * @param toChange - List of new layer names + * @param current - Index of the current layer + */ + public LayerUpdateEvent(ArrayList toChange, Index current) { + this.list = requireNonNull(toChange); + this.current = current; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/LoginStatusEvent.java b/src/main/java/seedu/address/commons/events/ui/LoginStatusEvent.java new file mode 100644 index 000000000000..f12155ce775f --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/LoginStatusEvent.java @@ -0,0 +1,35 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.events.BaseEvent; + +//@@author chivent + +/** + * An event that notifies StatusBarFooter regarding a login status change. + */ +public class LoginStatusEvent extends BaseEvent { + + public final String user; + public final boolean loggedIn; + + /** + * Constructor for LoginStatusEvent + * + * @param user The email of the logged in user + */ + public LoginStatusEvent(String user) { + this.user = requireNonNull(user); + if (this.user.isEmpty()) { + loggedIn = false; + } else { + loggedIn = true; + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/LogoutStatusEvent.java b/src/main/java/seedu/address/commons/events/ui/LogoutStatusEvent.java new file mode 100644 index 000000000000..6469394c13e3 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/LogoutStatusEvent.java @@ -0,0 +1,16 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +//@@author chivent + +/** + * An event that notifies StatusBarFooter regarding a login status change. + */ +public class LogoutStatusEvent extends BaseEvent { + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java deleted file mode 100644 index c5c8b9ce90ed..000000000000 --- a/src/main/java/seedu/address/commons/events/ui/PersonPanelSelectionChangedEvent.java +++ /dev/null @@ -1,26 +0,0 @@ -package seedu.address.commons.events.ui; - -import seedu.address.commons.events.BaseEvent; -import seedu.address.model.person.Person; - -/** - * Represents a selection change in the Person List Panel - */ -public class PersonPanelSelectionChangedEvent extends BaseEvent { - - - private final Person newSelection; - - public PersonPanelSelectionChangedEvent(Person newSelection) { - this.newSelection = newSelection; - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - public Person getNewSelection() { - return newSelection; - } -} diff --git a/src/main/java/seedu/address/commons/events/ui/UpdateFilmReelEvent.java b/src/main/java/seedu/address/commons/events/ui/UpdateFilmReelEvent.java new file mode 100644 index 000000000000..d8d41905c0fa --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/UpdateFilmReelEvent.java @@ -0,0 +1,31 @@ +package seedu.address.commons.events.ui; + +import static java.util.Objects.requireNonNull; + +import java.nio.file.Path; +import java.util.List; + +import seedu.address.commons.events.BaseEvent; + +//@@author chivent +/** + * An event that updates the list of images in film reel upon call events. + */ +public class UpdateFilmReelEvent extends BaseEvent { + + public final List paths; + + /** + * Constructor for LoginStatusEvent + * + * @param paths List of paths to new images + */ + public UpdateFilmReelEvent(List paths) { + this.paths = requireNonNull(paths); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalOperationException.java b/src/main/java/seedu/address/commons/exceptions/IllegalOperationException.java new file mode 100644 index 000000000000..96cf90624342 --- /dev/null +++ b/src/main/java/seedu/address/commons/exceptions/IllegalOperationException.java @@ -0,0 +1,22 @@ +//@@author j-lum +package seedu.address.commons.exceptions; + +/** + * Exception thrown when the user performs an invalid operation. + */ +public class IllegalOperationException extends Exception { + /** + * @param message should contain relevant information on the failed operation + */ + public IllegalOperationException(String message) { + super(message); + } + + /** + * @param message should contain relevant information on the failed operation + * @param cause of the main exception + */ + public IllegalOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java index b1e2767cdd92..d359e414a680 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/address/commons/util/FileUtil.java @@ -1,5 +1,6 @@ package seedu.address.commons.util; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; @@ -13,12 +14,18 @@ public class FileUtil { private static final String CHARSET = "UTF-8"; - public static boolean isFileExists(Path file) { - return Files.exists(file) && Files.isRegularFile(file); + /** + * Creates directories if missing + * @param file + */ + public static void createDirectoriesIfMissing(File file) { + if (!file.exists()) { + file.mkdirs(); + } } /** - * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, + * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String, String...)}, * otherwise returns false. * @param path A string representing the file path. Cannot be null. */ @@ -35,33 +42,18 @@ public static boolean isValidPath(String path) { * Creates a file if it does not exist along with its missing parent directories. * @throws IOException if the file or directory cannot be created. */ - public static void createIfMissing(Path file) throws IOException { - if (!isFileExists(file)) { - createFile(file); - } - } - - /** - * Creates a file if it does not exist along with its missing parent directories. - */ - public static void createFile(Path file) throws IOException { - if (Files.exists(file)) { - return; + public static void createIfMissing(File file) throws IOException { + if (!file.exists()) { + file.createNewFile(); } - - createParentDirsOfFile(file); - - Files.createFile(file); } /** - * Creates parent directories of file if it has a parent directory + * Deletes file if it exists */ - public static void createParentDirsOfFile(Path file) throws IOException { - Path parentDir = file.getParent(); - - if (parentDir != null) { - Files.createDirectories(parentDir); + public static void deleteIfAvaliable(File file) { + if (file.exists()) { + file.delete(); } } diff --git a/src/main/java/seedu/address/commons/util/ImageMagickUtil.java b/src/main/java/seedu/address/commons/util/ImageMagickUtil.java new file mode 100644 index 000000000000..ed307377cce2 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/ImageMagickUtil.java @@ -0,0 +1,352 @@ +package seedu.address.commons.util; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.logging.Logger; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.ChangeImageEvent; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.UserPrefs; +import seedu.address.model.canvas.Canvas; +import seedu.address.model.canvas.Layer; +import seedu.address.model.transformation.Transformation; +import seedu.address.storage.JsonConvertArgsStorage; + +/** + * An utility class that handles most of the low-level interaction with the ImageMagick executable. + * @author lancelotwillow + */ +public class ImageMagickUtil { + //initialize the paths used in the imageMagic + private static final int LINUX = 1; + private static final int WINDOWS = 2; + private static final int MAC = 3; + private static final String osName = System.getProperty("os.name").toLowerCase(); + private static String convertExecutablePath = ""; + private static String imageMagickPath = ImageMagickUtil.class.getResource("/imageMagic").getPath(); + private static String tmpPath = imageMagickPath + "/tmp"; + private static String commandSaveFolder; + + /** + * get the path of the package location + * @return + * @throws NoSuchElementException + */ + public static URL getImageMagickZipUrl(String osName) throws NoSuchElementException { + switch (getPlatform(osName)) { + case MAC: + return ImageMagickUtil.class.getResource("/imageMagic/package/mac/ImageMagick-7.0.8.zip"); + case WINDOWS: + return ImageMagickUtil.class.getResource("/imageMagic/package/win/ImageMagick-7.0.8-14.zip"); + default: + return ImageMagickUtil.class.getResource("/imageMagic/package/mac/ImageMagick-7.0.8.zip"); + } + } + + public static String getCommandSaveFolder() { + return commandSaveFolder; + } + + //these two methods are used for tesing only + public static void setTemporaryCommandForder(String folder) { + commandSaveFolder = folder; + } + + public static Path getTempFolderPath() { + return Paths.get(tmpPath); + } + + /** + * get the platform; + * @return + */ + public static int getPlatform(String osName) { + if (osName.contains("win")) { + return WINDOWS; + } else if (osName.contains("mac")) { + return MAC; + } else if (osName.contains("nux") || osName.contains("ubuntu")) { + return LINUX; + } + return 0; + } + + /** + * with a path and the transmission, return the bufferedimage processed. + * @param path + * @param transformation + * @return + * @throws ParseException + * @throws IOException + * @throws InterruptedException + */ + public static BufferedImage processImage(Path path, Transformation transformation, boolean isRaw) + throws ParseException, IOException, InterruptedException, IllegalArgumentException, + IllegalOperationException { + String modifiedFile = tmpPath + "/output.png"; + //create a processbuilder to blur the image + ArrayList args = new ArrayList<>(); + args.add(ImageMagickUtil.getConvertExecutablePath()); + args.add(path.toAbsolutePath().toString()); + args.add("-background"); + args.add("rgba(0,0,0,0)"); //HARDFIX! + if (!isRaw) { + ArrayList cmds = parseOperationArguments(transformation); + args.add("-" + cmds.remove(0)); + args.addAll(cmds); + } else { + ArrayList cmds = transformation.toList(); + cmds.remove(0); + args.addAll(cmds); + } + args.add(modifiedFile); + return runProcessBuilder(args, modifiedFile); + } + + /** + * parse the argument passing to the imageMagic to check the validation about the arguments + * @param transformation + * @return + * @throws IOException + * @throws ParseException + */ + private static ArrayList parseOperationArguments(Transformation transformation) + throws IOException, ParseException, IllegalArgumentException { + String operation = transformation.getOperation(); + if (!operation.startsWith("@")) { + return parseBuildInOperation(transformation); + } else { + return parseCustomisedOperation(transformation); + } + } + + /** + * . + * @param transformation + * @return + * @throws ParseException + * @throws IOException + */ + private static ArrayList parseBuildInOperation(Transformation transformation) + throws ParseException, IOException { + ArrayList trans = transformation.toList(); + String operation = transformation.getOperation(); + URL fileUrl = ImageMagickUtil.class.getResource("/imageMagic/commandTemplates/" + operation + ".json"); + if (fileUrl == null) { + throw new ParseException("Operation is invalid"); + } + //in order to get the template of the argument. + List cmds = JsonConvertArgsStorage.retrieveCommandTemplate(fileUrl, operation, "arg"); + int num = cmds.size(); + String template = cmds.toString(); + if (num != trans.size() - 1) { + throw new IllegalArgumentException("Invalid arguments, the arguments should be " + + operation + " " + template.substring(1, template.length() - 1)); + } + //get the pattern for the argument, check validation. + List patterns = JsonConvertArgsStorage.retrieveCommandTemplate(fileUrl, operation, "pattern"); + for (int i = 0; i < patterns.size(); i++) { + if (!trans.get(i + 1).matches(patterns.get(i))) { + throw new IllegalArgumentException("Invalid arguments, the arguments should be:" + + operation + " " + template.substring(1, template.length() - 1)); + } + } + return trans; + } + + /** + * . + * @param transformation + * @return + * @throws ParseException + * @throws IOException + */ + private static ArrayList parseCustomisedOperation(Transformation transformation) + throws ParseException, IOException { + String operation = transformation.getOperation().substring(1); + File file = new File(commandSaveFolder + "/" + operation + ".json"); + if (!file.exists()) { + throw new ParseException("Operation is invalid"); + } + return new ArrayList<>(JsonConvertArgsStorage.retrieveCommandArguments(file)); + } + + /** + * Given a list of arguments to ImageMagick, calls the actual ImageMagick executable with the output set to the + * path provided + * @param args An ArrayList of arguments, the first of which needs to be a legal ImageMagick executable. + * @param output An URL to the output file. + * @return + * @throws IOException + * @throws InterruptedException + */ + + public static BufferedImage runProcessBuilder(ArrayList args, String output) + throws IOException, InterruptedException, IllegalArgumentException, IllegalOperationException { + ProcessBuilder pb = new ProcessBuilder(args); + if (getPlatform(osName) == MAC) { + Map mp = pb.environment(); + mp.put("DYLD_LIBRARY_PATH", imageMagickPath + "/ImageMagick-7.0.8/lib/"); + } + if (getPlatform(osName) == LINUX || getPlatform(osName) == 0) { + throw new IllegalOperationException("Unsupported OS!"); + } + Process process = pb.start(); + process.waitFor(); + if (process.exitValue() != 0) { + throw new IllegalArgumentException("Process fails"); + } + FileInputStream is = new FileInputStream(output); + Image modifiedImage = new Image(is); + return SwingFXUtils.fromFXImage(modifiedImage, null); + } + + /** + * copy the imageMagick outside of the jarfile in order to call it. + * @author lancelotwillow + * @param userPrefs + * @throws IOException + * @throws InterruptedException + */ + public static void copyOutside(UserPrefs userPrefs, String osName) throws IOException, InterruptedException { + URL zipUrl = getImageMagickZipUrl(osName); + Path currentPath = userPrefs.getCurrDirectory(); + File zipFile = new File(currentPath.toString() + "/temp.zip"); + File tempFolder = new File(userPrefs.getCurrDirectory() + "/tempFolder"); + tempFolder.mkdir(); + ResourceUtil.copyResourceFileOut(zipUrl, zipFile); + switch (getPlatform(osName)) { + case MAC: + Process process = new ProcessBuilder( + "tar", "zxvf", zipFile.getPath(), "-C", currentPath.toString()).start(); + process.waitFor(); + //remove the __MACOSX folder in the mac + new ProcessBuilder("rm", "-rf", currentPath.toString() + "/__MACOSX").start(); + convertExecutablePath = currentPath.toString() + "/ImageMagick-7.0.8/bin/convert"; + break; + case WINDOWS: + ResourceUtil.unzipFolder(zipFile); + convertExecutablePath = currentPath.toString() + "/ImageMagick-7.0.8-14-portable-Q16-x64/convert.exe"; + break; + default: + } + imageMagickPath = currentPath.toString(); + tmpPath = tempFolder.getPath(); + commandSaveFolder = currentPath.toString() + "/PiconsoCommands"; + File commandFolder = new File(commandSaveFolder); + if (!(commandFolder.exists() && commandFolder.isDirectory())) { + commandFolder.mkdir(); + } + zipFile.delete(); + } + + public static String getConvertExecutablePath() { + if (convertExecutablePath != null) { + return convertExecutablePath; + } + throw new NoSuchElementException("The ImageMagick binaries cannot be found!"); + } + + //@@author j-lum + /** + * Creates a ProcessBuilder instance to merge/flatten layers. + * @param c - A canvas to be processed + * @return a buffered image with a merged canvas. + */ + public static BufferedImage processCanvas(Canvas c) throws IOException, InterruptedException, + IllegalOperationException { + ArrayList args = new ArrayList<>(); + String output = tmpPath + "/modified.png"; + args.add(getConvertExecutablePath()); + boolean pageSizeDefined = false; + + for (Layer l: c.getLayers()) { + if (!c.isCanvasAuto() && !pageSizeDefined) { + args.add("-page"); + args.add(String.format("%dx%d+%d+%d", c.getWidth(), c.getHeight(), l.getX(), l.getY())); + args.add(String.format("%s", l.getImage().getCurrentPath())); + pageSizeDefined = true; + } + args.add("-page"); + args.add(String.format("+%d+%d", l.getX(), l.getY())); + args.add(String.format("%s", l.getImage().getCurrentPath())); + } + + args.add("-background"); + args.add(String.format("%s", c.getBackgroundColor())); + + if (c.isCanvasAuto()) { + args.add("-layers"); + args.add("merge"); + } else { + args.add("-flatten"); + } + args.add(output); + System.out.println(args); + return runProcessBuilder(args, output); + } + + /** + * Saves the canvas to an output file + * @param c - A canvas to be processed + * *//* + public static void saveCanvas(Canvas c, Path outDirectory, String fileName) + throws IOException, InterruptedException, UnsupportedPlatformException { + ArrayList args = new ArrayList<>(); + String output = outDirectory + "/" + fileName; + args.add(getConvertExecutablePath()); + args.add("-size"); + args.add(String.format("%dx%d", c.getWidth(), c.getHeight())); + args.add("-background"); + args.add(c.getBackgroundColor()); + for (Layer l: c.getLayers()) { + args.add("-page"); + args.add(String.format("+%d+%d", l.getX(), l.getY())); + args.add(String.format("%s", l.getImage().getCurrentPath())); + } + if (c.isCanvasAuto()) { + args.add("-layers"); + args.add(" merge"); + } else { + args.add("-flatten"); + } + args.add(output); + System.out.println(output); + runProcessBuilder(args, output); + } +*/ + + /** + * Given any canvas, renders it to the target panel. + * @param c - Canvas to render + * @param logger - an instance of the logger + * @param target - the name of the ImagePanel to target + */ + public static void render(Canvas c, Logger logger, String target) { + try { + EventsCenter.getInstance().post( + new ChangeImageEvent( + SwingFXUtils.toFXImage( + ImageMagickUtil.processCanvas(c), null), target)); + } catch (IOException e) { + logger.severe(e.getMessage()); + } catch (InterruptedException | IllegalOperationException e) { + logger.severe(e.getMessage()); + } + + } + +} diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/address/commons/util/JsonUtil.java index 8ef609f055df..adc82e085fe5 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/address/commons/util/JsonUtil.java @@ -39,11 +39,11 @@ public class JsonUtil { .addSerializer(Level.class, new ToStringSerializer()) .addDeserializer(Level.class, new LevelDeserializer(Level.class))); - static void serializeObjectToJsonFile(Path jsonFile, T objectToSerialize) throws IOException { + protected static void serializeObjectToJsonFile(Path jsonFile, T objectToSerialize) throws IOException { FileUtil.writeToFile(jsonFile, toJsonString(objectToSerialize)); } - static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObjectToDeserialize) + protected static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObjectToDeserialize) throws IOException { return fromJsonString(FileUtil.readFromFile(jsonFile), classOfObjectToDeserialize); } diff --git a/src/main/java/seedu/address/commons/util/ResourceUtil.java b/src/main/java/seedu/address/commons/util/ResourceUtil.java new file mode 100644 index 000000000000..1059ca3df5f1 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/ResourceUtil.java @@ -0,0 +1,84 @@ +package seedu.address.commons.util; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * an Util to handle copying or unzipping the file outside of the resource when packing into jarfile + * @author lancelotwillow + */ +public class ResourceUtil { + + /** + * copy a file from the resource in the jar package outside + * @param url + * @param file + * @throws IOException when the file not exists + */ + public static void copyResourceFileOut(URL url, File file) throws IOException { + InputStream io = url.openStream(); + OutputStream os = new FileOutputStream(file); + int read; + byte[] bytes = new byte[1024]; + while ((read = io.read(bytes)) != -1) { + os.write(bytes, 0, read); + } + io.close(); + os.close(); + } + + /** + * unzip a folder to the directory the zip file stays + * @param zfile + * @throws IOException when the inputStream, outputStream, ZipStream cannot read anything + */ + public static void unzipFolder(File zfile) throws IOException { + //get the parent folder + String parentFolder = zfile.getParent() + "/"; + //initialize the input streams + FileInputStream fis = new FileInputStream(zfile); + ZipInputStream zis = new ZipInputStream(fis); + ZipEntry entry; + BufferedOutputStream bos; + boolean check = false; + while ((entry = zis.getNextEntry()) != null) { + //check the folder to be unzipped into exist or not + if (!check) { + String path = entry.getName(); + String folderName = path.split("/")[0]; + File unzippedFolder = new File(parentFolder + folderName); + if (!(unzippedFolder.exists() && unzippedFolder.isDirectory())) { + unzippedFolder.mkdir(); + } + check = true; + } + //make directory if the entry is a directory + if (entry.isDirectory()) { + File currentFile = new File(parentFolder + entry.getName()); + if (!currentFile.exists()) { + currentFile.mkdirs(); + } + } else { + //write the file to be unzipped + FileOutputStream fos = new FileOutputStream(parentFolder + entry.getName()); + bos = new BufferedOutputStream(fos); + byte[] buf = new byte[2048]; + int len; + while ((len = zis.read(buf)) != -1) { + bos.write(buf, 0, len); + } + zis.closeEntry(); + bos.close(); + } + } + zis.close(); + } +} diff --git a/src/main/java/seedu/address/commons/util/XmlUtil.java b/src/main/java/seedu/address/commons/util/XmlUtil.java deleted file mode 100644 index a78cd15b7f0c..000000000000 --- a/src/main/java/seedu/address/commons/util/XmlUtil.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.commons.util; - -import static java.util.Objects.requireNonNull; - -import java.io.FileNotFoundException; -import java.nio.file.Files; -import java.nio.file.Path; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; - -/** - * Helps with reading from and writing to XML files. - */ -public class XmlUtil { - - /** - * Returns the xml data in the file as an object of the specified type. - * - * @param file Points to a valid xml file containing data that match the {@code classToConvert}. - * Cannot be null. - * @param classToConvert The class corresponding to the xml data. - * Cannot be null. - * @throws FileNotFoundException Thrown if the file is missing. - * @throws JAXBException Thrown if the file is empty or does not have the correct format. - */ - @SuppressWarnings("unchecked") - public static T getDataFromFile(Path file, Class classToConvert) - throws FileNotFoundException, JAXBException { - - requireNonNull(file); - requireNonNull(classToConvert); - - if (!FileUtil.isFileExists(file)) { - throw new FileNotFoundException("File not found : " + file.toAbsolutePath()); - } - - JAXBContext context = JAXBContext.newInstance(classToConvert); - Unmarshaller um = context.createUnmarshaller(); - - return ((T) um.unmarshal(file.toFile())); - } - - /** - * Saves the data in the file in xml format. - * - * @param file Points to a valid xml file containing data that match the {@code classToConvert}. - * Cannot be null. - * @throws FileNotFoundException Thrown if the file is missing. - * @throws JAXBException Thrown if there is an error during converting the data - * into xml and writing to the file. - */ - public static void saveDataToFile(Path file, T data) throws FileNotFoundException, JAXBException { - - requireNonNull(file); - requireNonNull(data); - - if (!Files.exists(file)) { - throw new FileNotFoundException("File not found : " + file.toAbsolutePath()); - } - - JAXBContext context = JAXBContext.newInstance(data.getClass()); - Marshaller m = context.createMarshaller(); - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); - - m.marshal(data, file.toFile()); - } - -} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 8b34b862039a..669bad33d0df 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -1,10 +1,8 @@ package seedu.address.logic; -import javafx.collections.ObservableList; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Person; /** * API of the Logic component @@ -19,9 +17,6 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); - /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ ListElementPointer getHistorySnapshot(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9aff86fc33dc..3717c031dc0e 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -2,16 +2,14 @@ import java.util.logging.Logger; -import javafx.collections.ObservableList; import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; +import seedu.address.logic.parser.PiconsoParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; -import seedu.address.model.person.Person; /** * The main LogicManager of the app. @@ -21,30 +19,25 @@ public class LogicManager extends ComponentManager implements Logic { private final Model model; private final CommandHistory history; - private final AddressBookParser addressBookParser; + private final PiconsoParser piconsoParser; public LogicManager(Model model) { this.model = model; history = new CommandHistory(); - addressBookParser = new AddressBookParser(); + piconsoParser = new PiconsoParser(); } @Override public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); try { - Command command = addressBookParser.parseCommand(commandText); + Command command = piconsoParser.parseCommand(commandText); return command.execute(model, history); } finally { history.add(commandText); } } - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } - @Override public ListElementPointer getHistorySnapshot() { return new ListElementPointer(history.getHistory()); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index d88e831ff1ce..000000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ApplyCommand.java b/src/main/java/seedu/address/logic/commands/ApplyCommand.java new file mode 100644 index 000000000000..921247d4a367 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ApplyCommand.java @@ -0,0 +1,86 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.awt.image.BufferedImage; +import java.net.URL; +import java.util.Arrays; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.transformation.Transformation; + + +/** + * @@author lancelotwillow + * the class to execute the apply command that do the modification of the image + */ +public class ApplyCommand extends Command { + + public static final String COMMAND_WORD = "apply"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Apply a transformation to the image.\n" + + "Parameters: operationName argument1 argument2 ...\n" + + "Example: " + COMMAND_WORD + " blur 1x8"; + public static final String MESSAGE_USAGE_RAW = COMMAND_WORD + + ": Apply a transformation to the image by passing the values directly to ImageMagick.\n" + + "Parameters: argument1 argument2 ...\n" + + "Example: " + COMMAND_WORD + " raw +noise gaussian"; + //the path of the json file containing the arguments of the apply command + public static final URL SINGLE_COMMAND_TEMPLATE_PATH = + ImageMagickUtil.class.getResource("/imageMagic/commandTemplates"); + private static final Logger logger = LogsCenter.getLogger(ApplyCommand.class); + + private Transformation transformation; + private boolean isRaw; + + + /** + * the constructor take the path of the JSON file of the detail of the apply operation + * @param transformation contains the operation to be processed to the image + */ + public ApplyCommand(Transformation transformation) { + this.transformation = transformation; + this.isRaw = false; + } + + public ApplyCommand(String[] args) { + this.transformation = new Transformation(Arrays.toString(args), args); + this.isRaw = true; + } + + /** + * build a new processbuilder and initialize witht the commands need to the apply command + * @param model {@code Model} which the command should operate on. + * @param history {@code CommandHistory} which the command should operate on. + * @return + * @throws CommandException + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + try { + BufferedImage modifiedImage = ImageMagickUtil.processImage(model.getCurrentPreviewImagePath(), + transformation, isRaw); + model.addTransformation(isRaw ? new Transformation(transformation.getOperation()) : transformation); + model.updateCurrentPreviewImage(modifiedImage); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + } catch (IllegalOperationException e) { + throw new CommandException(e.getMessage()); + } catch (Exception e) { + throw new CommandException(isRaw ? "Invalid operation!" : e.getMessage()); + } + return new CommandResult("Transformation applied!"); + } + + @Override + public boolean equals(Object object) { + ApplyCommand command = (ApplyCommand) object; + return command == this || transformation.equals(command.transformation); + } +} diff --git a/src/main/java/seedu/address/logic/commands/CdCommand.java b/src/main/java/seedu/address/logic/commands/CdCommand.java new file mode 100644 index 000000000000..b385d24f886a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CdCommand.java @@ -0,0 +1,78 @@ +//@@author benedictcss +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; + + +/** + * Changes the current directory. + */ +public class CdCommand extends Command { + + public static final String COMMAND_WORD = "cd"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Changes the current directory.\n" + + "Parameters: FILEPATH (existing directory)\n" + + "Example: " + COMMAND_WORD + " Desktop/piconso"; + + public static final String MESSAGE_FAILURE = "The system cannot find the path specified."; + + private final Path toDirectories; + + public CdCommand(Path directories) { + this.toDirectories = directories; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + + String currDirectory = model.getCurrDirectory().toString(); + String newDir = currDirectory + "/" + toDirectories.toString(); + + Path newCurrDirectory = Paths.get(""); + File dir; + try { + if (toDirectories.isAbsolute()) { + dir = toDirectories.toFile(); + } else { + dir = new File(newDir); + } + + if (!dir.isDirectory()) { + return new CommandResult(MESSAGE_FAILURE); + } + newCurrDirectory = dir.toPath().toRealPath(); + model.updateCurrDirectory(newCurrDirectory); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + + return new CommandResult(newCurrDirectory.toString() + "\n" + + String.format(Messages.MESSAGE_TOTAL_IMAGES_IN_DIR, model.getTotalImagesInDir()) + + String.format(Messages.MESSAGE_CURRENT_IMAGES_IN_BATCH, + Math.min(model.getDirectoryImageList().size(), OpenCommand.BATCH_SIZE))); + } + + public Path getPath() { + return this.toDirectories; + } + + @Override + + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CdCommand // instanceof handles nulls + && toDirectories.equals(((CdCommand) other).toDirectories)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 1f85bcfe85a8..000000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.logic.CommandHistory; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.resetData(new AddressBook()); - model.commitAddressBook(); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/CreateApplyCommand.java b/src/main/java/seedu/address/logic/commands/CreateApplyCommand.java new file mode 100644 index 000000000000..f8a9cb193444 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CreateApplyCommand.java @@ -0,0 +1,95 @@ +package seedu.address.logic.commands; + +import java.io.IOException; +import java.net.URL; +import java.util.Iterator; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.transformation.Transformation; +import seedu.address.storage.JsonConvertArgsStorage; + +/** + * @@author lancelotwillow + * the class to create the convert command + */ +public class CreateApplyCommand extends Command { + + public static final String COMMAND_WORD = "create"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a new custom command.\n" + + "Parameters: name operation|argument1|argument2 ...\n" + + "Example: " + COMMAND_WORD + " blurgray blur|0x8 colorspace|GRAY"; + private List cmds; + private String name; + + public CreateApplyCommand(String name, List cmds) { + if (cmds.isEmpty()) { + throw new IllegalArgumentException("Empty parameters! Please provide transformations to store."); + } else { + this.cmds = cmds; + this.name = name; + } + } + + /** + * to check whether the single argument tag is valid or not + * @param transformation + * @throws IllegalArgumentException + */ + private void checkSingleValidation(Transformation transformation) throws IllegalArgumentException, IOException { + //just a template, not only this + String operation = transformation.toList().get(0); + URL fileUrl = CreateApplyCommand.class.getResource("/imageMagic/commandTemplates/" + operation + ".json"); + if (fileUrl == null) { + throw new IllegalArgumentException(); + } + List patterns = JsonConvertArgsStorage.retrieveCommandTemplate(fileUrl, operation, "pattern"); + List trans = transformation.toList(); + if (trans.size() != patterns.size() + 1) { + throw new IllegalArgumentException(Messages.MESSAGE_INVALID_OPERATION_ARGUMENTS); + } + for (int i = 0; i < patterns.size(); i++) { + if (!trans.get(i + 1).matches(patterns.get(i))) { + throw new IllegalArgumentException(Messages.MESSAGE_INVALID_OPERATION_ARGUMENTS); + } + } + } + + + /** + * to check the validation of the whole argument list + */ + private void checkValidation() throws CommandException { + Iterator iter = cmds.iterator(); + while (iter.hasNext()) { + try { + checkSingleValidation(iter.next()); + } catch (IllegalArgumentException | IOException e) { + throw new CommandException(Messages.MESSAGE_INVALID_OPERATION_ARGUMENTS); + } + } + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + try { + checkValidation(); + JsonConvertArgsStorage.storeArgument(name, cmds, ImageMagickUtil.getCommandSaveFolder()); + } catch (IOException e) { + throw new CommandException(Messages.MESSAGE_INVALID_OPERATION_ARGUMENTS); + } + return new CommandResult("Successfully created " + name); + } + + @Override + public boolean equals(Object object) { + CreateApplyCommand command = (CreateApplyCommand) object; + return command == this || name.equals(command.name) && cmds.equals(command.cmds); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index a20e9d49eac7..000000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,55 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index dc782d8e230f..000000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,228 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.updatePerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * 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 EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index e848fa918964..64d8874d048e 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -1,7 +1,10 @@ package seedu.address.logic.commands; +import java.nio.file.InvalidPathException; + import seedu.address.commons.core.EventsCenter; import seedu.address.commons.events.ui.ExitAppRequestEvent; +import seedu.address.commons.util.ImageMagickUtil; import seedu.address.logic.CommandHistory; import seedu.address.model.Model; @@ -12,10 +15,15 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Piconso as requested ..."; @Override public CommandResult execute(Model model, CommandHistory history) { + try { + ImageMagickUtil.getTempFolderPath().toFile().delete(); + } catch (InvalidPathException e) { + e.printStackTrace(); + } EventsCenter.getInstance().post(new ExitAppRequestEvent()); return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT); } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index beb178e3a3f5..000000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,43 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/address/logic/commands/HistoryCommand.java deleted file mode 100644 index f1541fb57f20..000000000000 --- a/src/main/java/seedu/address/logic/commands/HistoryCommand.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.Collections; -import java.util.List; - -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; - -/** - * Lists all the commands entered by user from the start of app launch. - */ -public class HistoryCommand extends Command { - - public static final String COMMAND_WORD = "history"; - public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; - public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(history); - List previousCommands = history.getHistory(); - - if (previousCommands.isEmpty()) { - return new CommandResult(MESSAGE_NO_HISTORY); - } - - Collections.reverse(previousCommands); - return new CommandResult(String.format(MESSAGE_SUCCESS, String.join("\n", previousCommands))); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 6d44824c7d1b..000000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/LoginCommand.java b/src/main/java/seedu/address/logic/commands/LoginCommand.java new file mode 100644 index 000000000000..975eea9f4727 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LoginCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_CONNECTION_FAILURE; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.NewResultAvailableEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +//@@author chivent + +/** + * Logs in user + */ +public class LoginCommand extends Command { + + public static final String COMMAND_WORD = "login"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Re-directs user " + + "to log into Google Photos (requires an internet connection). "; + + protected static final String MESSAGE_LAUNCHED = "You will be re-directed to a login window shortly..."; + private static final String MESSAGE_LOGGED_IN = "Logged in as %s."; + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + + try { + if (model.getUserLoggedIn() == null) { + CompletableFuture.supplyAsync(() -> { + try { + EventsCenter.getInstance().post(new NewResultAvailableEvent(this.initPhotoHandler(model))); + return true; + } catch (CommandException e) { + EventsCenter.getInstance().post(new NewResultAvailableEvent(MESSAGE_CONNECTION_FAILURE)); + return false; + } + }).completeOnTimeout(true, 3, TimeUnit.MINUTES); + return new CommandResult(MESSAGE_LAUNCHED); + } else { + return new CommandResult(String.format(MESSAGE_LOGGED_IN, model.getUserLoggedIn())); + } + } catch (Exception ex) { + return new CommandResult(MESSAGE_CONNECTION_FAILURE); + } + } + + /** + * Method to launch creation/checking of photoHandler + * @param model model to be based upon + * @return feedback to display in resultsdisplay + */ + private String initPhotoHandler(Model model) throws CommandException { + if (model.getPhotoHandler(false) == null) { + return MESSAGE_CONNECTION_FAILURE; + } + return String.format(MESSAGE_LOGGED_IN, model.getUserLoggedIn()); + } +} diff --git a/src/main/java/seedu/address/logic/commands/LogoutCommand.java b/src/main/java/seedu/address/logic/commands/LogoutCommand.java new file mode 100644 index 000000000000..e2e0fcf80671 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LogoutCommand.java @@ -0,0 +1,44 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.LogoutStatusEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.model.google.PhotosLibraryClientFactory; + +//@@author chivent + +/** + * Logs in user + */ +public class LogoutCommand extends Command { + + public static final String COMMAND_WORD = "logout"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Log out from Google Photos"; + + protected static final String MESSAGE_LOGGED_OUT = "Successfully logged out."; + protected static final String MESSAGE_NONE = "There is no account logged into."; + private static final String MESSAGE_ERROR = "Error while attempting to logout."; + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + + model.setPhotoHandler(null); + + try { + if (PhotosLibraryClientFactory.checkUserLogin()) { + PhotosLibraryClientFactory.logoutUserIfPossible(); + EventsCenter.getInstance().post(new LogoutStatusEvent()); + return new CommandResult(MESSAGE_LOGGED_OUT); + } else { + return new CommandResult(MESSAGE_NONE); + } + } catch (Exception ex) { + return new CommandResult(MESSAGE_ERROR); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/LsCommand.java b/src/main/java/seedu/address/logic/commands/LsCommand.java new file mode 100644 index 000000000000..0844b7930a11 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/LsCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_EMPTY_DIR; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_FILE_DIR; + +import java.io.File; +import javax.activation.MimetypesFileTypeMap; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.UpdateFilmReelEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; + +//@@author chivent + +/** + * Lists all files in current directory. + */ +public class LsCommand extends Command { + + public static final String COMMAND_WORD = "ls"; + + public static final String MESSAGE_FAILURE = "Unable to list files in current directory"; + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + StringBuffer fileNames = new StringBuffer(); + + File dir = new File(model.getCurrDirectory().toString()); + + try { + File[] fileList = dir.listFiles(); + for (File file : fileList) { + if (file.isFile()) { + + String mimetype = new MimetypesFileTypeMap().getContentType(file); + // only list if is image + if ((mimetype.split("/")[0]).equals("image")) { + fileNames.append(file.getName()); + fileNames.append(" \n"); + } + + } else if (file.isDirectory()) { + fileNames.append(file.getName()); + fileNames.append(" \n"); + } + } + + if (fileNames.toString().isEmpty()) { + fileNames.append(MESSAGE_EMPTY_DIR); + } else { + EventsCenter.getInstance().post(new UpdateFilmReelEvent(model.getDirectoryImageList())); + } + + return new CommandResult(fileNames.toString()); + } catch (NullPointerException e) { + return new CommandResult(MESSAGE_INVALID_FILE_DIR); + } catch (Exception ex) { + return new CommandResult(ex.getMessage()); + } + + } +} diff --git a/src/main/java/seedu/address/logic/commands/NextCommand.java b/src/main/java/seedu/address/logic/commands/NextCommand.java new file mode 100644 index 000000000000..99e2fa3494bf --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/NextCommand.java @@ -0,0 +1,36 @@ +//@@author benedictcss +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Lists the next batch of photos in the directory. + */ +public class NextCommand extends Command { + + public static final String COMMAND_WORD = "next"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Retrieves the next batch of photos for viewing.\n" + + "Example: " + COMMAND_WORD; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (model.numOfRemainingImagesInDir() <= OpenCommand.BATCH_SIZE) { + throw new CommandException(Messages.MESSAGE_NO_MORE_NEXT_IMAGES); + } + + model.updateImageListNextBatch(); + + return new CommandResult((String.format(Messages.MESSAGE_TOTAL_IMAGES_IN_DIR, model.getTotalImagesInDir()) + + (String.format(Messages.MESSAGE_CURRENT_BATCH_IN_IMAGE_LIST, model.getCurrBatchPointer() + 1, + model.getCurrBatchPointer() + Math.min(model.numOfRemainingImagesInDir(), OpenCommand.BATCH_SIZE)) + + (String.format(Messages.MESSAGE_CURRENT_IMAGES_IN_BATCH, model.getDirectoryImageList().size()))))); + } +} diff --git a/src/main/java/seedu/address/logic/commands/OpenCommand.java b/src/main/java/seedu/address/logic/commands/OpenCommand.java new file mode 100644 index 000000000000..3fdaad03d88d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/OpenCommand.java @@ -0,0 +1,82 @@ +// @@author benedictcss +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.nio.file.Path; +import java.util.List; + +import javafx.scene.image.Image; +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.ChangeImageEvent; +import seedu.address.commons.events.ui.FilmReelSelectionChangeEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Selects the image identified by the index number in the current batch. + */ +public class OpenCommand extends Command { + + public static final String COMMAND_WORD = "open"; + public static final int BATCH_SIZE = 10; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Opens the image identified by the index number in the current batch.\n" + + "Parameters: INDEX (1 - 10)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_OPEN_IMAGE_SUCCESS = "Selected image: %1$s"; + + private final Index targetIndex; + + public OpenCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + List dirImageList = model.getDirectoryImageList(); + + if (targetIndex.getZeroBased() >= model.numOfRemainingImagesInDir()) { + throw new CommandException(Messages.MESSAGE_INDEX_END_OF_IMAGE_LIST); + } else if (targetIndex.getZeroBased() >= BATCH_SIZE) { + throw new CommandException(Messages.MESSAGE_INDEX_EXCEED_MAX_BATCH_SIZE); + } + + + Path openedImagePath = dirImageList.get(targetIndex.getZeroBased()); + + try { + String openedImage = openedImagePath.toString(); + FileInputStream fis = new FileInputStream(openedImage); + Image img = new Image(fis); + + model.updateCurrentOriginalImage(img, openedImagePath); + EventsCenter.getInstance().post(new FilmReelSelectionChangeEvent(targetIndex.getZeroBased())); + EventsCenter.getInstance().post(new ChangeImageEvent(img, "preview")); + EventsCenter.getInstance().post(new ChangeImageEvent(img, "original")); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + return new CommandResult(String.format(MESSAGE_OPEN_IMAGE_SUCCESS, targetIndex.getOneBased()) + + " of " + Math.min(OpenCommand.BATCH_SIZE, model.getDirectoryImageList().size()) + "\n" + + "Image opened: " + openedImagePath.getFileName().toString()); + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof OpenCommand // instanceof handles nulls + && targetIndex.equals(((OpenCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/PrevCommand.java b/src/main/java/seedu/address/logic/commands/PrevCommand.java new file mode 100644 index 000000000000..dafa63cb635c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/PrevCommand.java @@ -0,0 +1,36 @@ +//@@author benedictcss +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Lists the next batch of photos in the directory. + */ +public class PrevCommand extends Command { + + public static final String COMMAND_WORD = "prev"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Retrieves the previous batch of photos for viewing.\n" + + "Example: " + COMMAND_WORD; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (model.getCurrBatchPointer() < OpenCommand.BATCH_SIZE) { + throw new CommandException(Messages.MESSAGE_NO_MORE_PREV_IMAGES); + } + + model.updateImageListPrevBatch(); + + return new CommandResult((String.format(Messages.MESSAGE_TOTAL_IMAGES_IN_DIR, model.getTotalImagesInDir()) + + (String.format(Messages.MESSAGE_CURRENT_BATCH_IN_IMAGE_LIST, model.getCurrBatchPointer() + 1, + model.getCurrBatchPointer() + Math.min(model.numOfRemainingImagesInDir(), OpenCommand.BATCH_SIZE)) + + (String.format(Messages.MESSAGE_CURRENT_IMAGES_IN_BATCH, model.getDirectoryImageList().size()))))); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoAllCommand.java b/src/main/java/seedu/address/logic/commands/RedoAllCommand.java new file mode 100644 index 000000000000..afef2d3a68a4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoAllCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +//@@author ihwk1996 +import static java.util.Objects.requireNonNull; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + +/** + * Reverts the {@code model}'s previewImageManager to its previously undone state. + */ +public class RedoAllCommand extends Command { + + public static final String COMMAND_WORD = "redo-all"; + public static final String MESSAGE_SUCCESS = "All transformations successfully redone"; + public static final String MESSAGE_FAILURE = "No more transformations to redo"; + + private static final Logger logger = LogsCenter.getLogger(RedoAllCommand.class); + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (!model.canRedoPreviewImage()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.redoAllPreviewImage(); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..e908ef0aecce 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -1,31 +1,37 @@ package seedu.address.logic.commands; +//@@author ihwk1996 import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; /** - * Reverts the {@code model}'s address book to its previously undone state. + * Reverts the {@code model}'s current layer's previewImage to its previously undone state. */ public class RedoCommand extends Command { public static final String COMMAND_WORD = "redo"; - public static final String MESSAGE_SUCCESS = "Redo success!"; - public static final String MESSAGE_FAILURE = "No more commands to redo!"; + public static final String MESSAGE_SUCCESS = "Transformation successfully redone"; + public static final String MESSAGE_FAILURE = "No more transformations to redo"; + + private static final Logger logger = LogsCenter.getLogger(RedoCommand.class); @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (!model.canRedoAddressBook()) { + if (!model.canRedoPreviewImage()) { throw new CommandException(MESSAGE_FAILURE); } - model.redoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.redoPreviewImage(); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/SaveCommand.java b/src/main/java/seedu/address/logic/commands/SaveCommand.java new file mode 100644 index 000000000000..cf7b299e3ed3 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SaveCommand.java @@ -0,0 +1,90 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * the command to save the current preview + * + * @author lancelotwillow + */ +public class SaveCommand extends Command { + public static final String COMMAND_WORD = "save"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": save the current preview image with the name specified.\n" + + "Parameters: fileName\n" + + "Example: " + COMMAND_WORD + " modified.png"; + public static final String OUTPUT_FAILURE = "Saving file as %s failed!"; + private String fileName; + private String format; + private boolean originalFile; + + /** + * @param fileName is the name of the file saved. + */ + public SaveCommand(String fileName) { + this.fileName = fileName; + String[] parts = fileName.split("\\."); + format = parts[1]; + originalFile = false; + } + + public SaveCommand() { + originalFile = true; + } + + /** + * Build a new processbuilder and initialize with the commands need to the convert command + * + * @param model {@code Model} which the command should operate on. + * @param history {@code CommandHistory} which the command should operate on. + * @return + * @throws CommandException + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + try { + File saveFile; + if (!originalFile) { + saveFile = new File(model.getCurrDirectory().toString() + "/" + fileName); + if (saveFile.exists()) { + throw new CommandException(Messages.MESSAGE_DUPLICATED_IMAGE); + } + } else { + saveFile = model.getCurrentOriginalImage().toFile(); + fileName = saveFile.getName(); + String[] parts = saveFile.getName().split("\\."); + format = parts[parts.length - 1]; + } + BufferedImage savedImage = ImageMagickUtil.processCanvas(model.getCanvas()); + fileName = saveFile.getName(); + ImageIO.write(savedImage, format, saveFile); + } catch (IllegalOperationException e) { + throw new CommandException(e.getMessage()); + } catch (IOException | InterruptedException e) { + throw new CommandException(String.format(OUTPUT_FAILURE, fileName)); + } + model.updateEntireImageList(); + return new CommandResult(String.format("%s successfully saved!", fileName)); + } + + @Override + public boolean equals(Object object) { + SaveCommand command = (SaveCommand) object; + return command == this || fileName.equals(command.fileName); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java deleted file mode 100644 index f5e8c1a8722e..000000000000 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.EventsCenter; -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Selects a person identified using it's displayed index from the address book. - */ -public class SelectCommand extends Command { - - public static final String COMMAND_WORD = "select"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Selects the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; - - private final Index targetIndex; - - public SelectCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - - List filteredPersonList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= filteredPersonList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - EventsCenter.getInstance().post(new JumpToListRequestEvent(targetIndex)); - return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased())); - - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof SelectCommand // instanceof handles nulls - && targetIndex.equals(((SelectCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/UndoAllCommand.java b/src/main/java/seedu/address/logic/commands/UndoAllCommand.java new file mode 100644 index 000000000000..732e32f0697d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoAllCommand.java @@ -0,0 +1,38 @@ +package seedu.address.logic.commands; + +//@@author ihwk1996 +import static java.util.Objects.requireNonNull; + +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Reverts the {@code model}'s previewImageManager to its previous state. + */ +public class UndoAllCommand extends Command { + + public static final String COMMAND_WORD = "undo-all"; + public static final String MESSAGE_SUCCESS = "All transformations successfully undone"; + public static final String MESSAGE_FAILURE = "No more transformations to undo"; + + private static final Logger logger = LogsCenter.getLogger(UndoAllCommand.class); + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (!model.canUndoPreviewImage()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.undoAllPreviewImage(); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index 40441264f346..41b1346a21fd 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -1,31 +1,38 @@ package seedu.address.logic.commands; +//@@author ihwk1996 import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; /** - * Reverts the {@code model}'s address book to its previous state. + * Reverts the {@code model}'s current layer's previewImage to its previous state. */ public class UndoCommand extends Command { public static final String COMMAND_WORD = "undo"; - public static final String MESSAGE_SUCCESS = "Undo success!"; - public static final String MESSAGE_FAILURE = "No more commands to undo!"; + public static final String MESSAGE_SUCCESS = "Transformation successfully undone"; + public static final String MESSAGE_FAILURE = "No more transformations to undo"; + + private static final Logger logger = LogsCenter.getLogger(UndoCommand.class); @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (!model.canUndoAddressBook()) { + if (!model.canUndoPreviewImage()) { throw new CommandException(MESSAGE_FAILURE); } - model.undoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.undoPreviewImage(); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/canvas/CanvasAutoResizeCommand.java b/src/main/java/seedu/address/logic/commands/canvas/CanvasAutoResizeCommand.java new file mode 100644 index 000000000000..adb31cbe1c26 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/canvas/CanvasAutoResizeCommand.java @@ -0,0 +1,54 @@ +package seedu.address.logic.commands.canvas; + +//@@author j-lum +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + + +/** + * Handles the toggling of canvas auto-resize. + * Commands are in the format : canvas auto-resize [ON/OFF] (case-insensitive). + * When turned on, the canvas will stretch to accommodate all layers regardless of canvas size. + */ + +public class CanvasAutoResizeCommand extends CanvasCommand { + public static final String TYPE = COMMAND_WORD + " auto-resize"; + + public static final String MESSAGE_USAGE = "Usage of canvas auto-resize: " + + "\n- " + TYPE + " [ON/OFF]: " + "Turns auto-resize on or off." + + "\n\tExample: " + TYPE + " off, turns auto-resize off."; + + public static final String OUTPUT_SUCCESS = "Auto-resize has been turned %s."; + public static final String OUTPUT_FAILURE = "Invalid operation %s!"; + + private static final Logger logger = LogsCenter.getLogger(CanvasAutoResizeCommand.class); + + public CanvasAutoResizeCommand(String args) { + super(args); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (args.equalsIgnoreCase("on")) { + model.setCanvasAuto(true); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + return new CommandResult(String.format(OUTPUT_SUCCESS, args.toLowerCase())); + } + if (args.equalsIgnoreCase("off")) { + model.setCanvasAuto(false); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + return new CommandResult(String.format(OUTPUT_SUCCESS, args.toLowerCase())); + } + throw new CommandException(String.format(OUTPUT_FAILURE, args.toLowerCase()) + + "\n\n" + + MESSAGE_USAGE + ); + } +} diff --git a/src/main/java/seedu/address/logic/commands/canvas/CanvasBgcolorCommand.java b/src/main/java/seedu/address/logic/commands/canvas/CanvasBgcolorCommand.java new file mode 100644 index 000000000000..08e5b0b97bfa --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/canvas/CanvasBgcolorCommand.java @@ -0,0 +1,50 @@ +package seedu.address.logic.commands.canvas; + +//@@author j-lum +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles the changing of background color. + * Commands are in the format : canvas bgcolor [`none` | hex color code | rgba/hsla color code]. + */ + +public class CanvasBgcolorCommand extends CanvasCommand { + public static final String TYPE = COMMAND_WORD + " bgcolor"; + public static final String MESSAGE_USAGE = "Usage of canvas bgcolor: " + + "\n- " + TYPE + " [color]: " + "Changes the background color to the color specified." + + "\n\tExample: " + TYPE + " rgba(0,255,0,0.7) - Changes the background color to" + + " lime-green with 70% opacity."; + + public static final String OUTPUT_SUCCESS = "Background color is now: %s."; + public static final String OUTPUT_FAILURE = "Invalid colour %s!"; + + private static final String HEX_REGEX = "^#(?:[0-9a-f]{3}){1,2}$"; + private static final String VERBOSE_REGEX = "(#([\\da-f]{3}){1,2}|" + + "(rgb|hsl)a\\((\\d{1,3}%?,\\s?){3}(1|0?\\.\\d+)\\)|(rgb|hsl)\\(\\d{1,3}%?(,\\s?\\d{1,3}%?){2}\\))"; + private static final String NONE_REGEX = "(none)"; + + private static final Logger logger = LogsCenter.getLogger(CanvasBgcolorCommand.class); + + public CanvasBgcolorCommand(String args) { + super(args); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (args.matches(HEX_REGEX) || args.matches(VERBOSE_REGEX) || args.matches(NONE_REGEX)) { + model.setBackgroundColor(args); + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + return new CommandResult(String.format(OUTPUT_SUCCESS, args)); + } + throw new CommandException(String.format(OUTPUT_FAILURE, args) + + "\n\n" + + MESSAGE_USAGE); + } +} diff --git a/src/main/java/seedu/address/logic/commands/canvas/CanvasCommand.java b/src/main/java/seedu/address/logic/commands/canvas/CanvasCommand.java new file mode 100644 index 000000000000..f56b52fc157c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/canvas/CanvasCommand.java @@ -0,0 +1,18 @@ +package seedu.address.logic.commands.canvas; + +import seedu.address.logic.commands.Command; + +//@@author j-lum + +/** + * Parent for all Canvas commands + */ +public abstract class CanvasCommand extends Command { + + public static final String COMMAND_WORD = "canvas"; + protected String args; + + CanvasCommand(String args) { + this.args = args; + } +} diff --git a/src/main/java/seedu/address/logic/commands/canvas/CanvasSizeCommand.java b/src/main/java/seedu/address/logic/commands/canvas/CanvasSizeCommand.java new file mode 100644 index 000000000000..9b83438d5208 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/canvas/CanvasSizeCommand.java @@ -0,0 +1,66 @@ +package seedu.address.logic.commands.canvas; + +//@@author j-lum +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + + + +/** + * Handles the changing and echoing of Canvas size. + * Commands are in the format - canvas size (height x width). + * If the optional (height x width) is omitted, the current height and width is displayed instead. + */ + +public class CanvasSizeCommand extends CanvasCommand { + public static final String TYPE = COMMAND_WORD + " size"; + public static final String MESSAGE_USAGE = "Usage of canvas size: " + + "\n- " + TYPE + " (SIZE): " + "Resizes the canvas if size is provided. Prints the current size otherwise." + + "\n\tExample: " + TYPE + " 800x600, sets the canvas size to 800px width and 600px height."; + + public static final String OUTPUT_SUCCESS = "Canvas size is now %d by %d."; + public static final String OUTPUT_FAILURE = "Invalid size provided!"; + + private static final Logger logger = LogsCenter.getLogger(CanvasSizeCommand.class); + + + public CanvasSizeCommand(String args) { + super(args); + } + + @Override + + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (args == null) { + return new CommandResult(String.format(OUTPUT_SUCCESS, + model.getCanvasWidth(), model.getCanvasHeight())); + } + String[] argumentArray = args.trim().split("x", 2); + + int newWidth; + int newHeight; + try { + newWidth = Integer.parseInt(argumentArray[0]); + newHeight = Integer.parseInt((argumentArray.length > 1) ? argumentArray[1] : ""); + if (newWidth <= 0 | newHeight <= 0) { + throw new NumberFormatException(); + } + } catch (NumberFormatException e) { + throw new CommandException(OUTPUT_FAILURE + + "\n\n" + + MESSAGE_USAGE); + } + model.setCanvasSize(newHeight, newWidth); + + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + + return new CommandResult(String.format(OUTPUT_SUCCESS, + model.getCanvasWidth(), model.getCanvasHeight())); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java index a16bd14f2cde..4120dca34171 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java @@ -1,5 +1,7 @@ package seedu.address.logic.commands.exceptions; +import seedu.address.logic.commands.Command; + /** * Represents an error which occurs during execution of a {@link Command}. */ @@ -7,11 +9,4 @@ public class CommandException extends Exception { public CommandException(String message) { super(message); } - - /** - * Constructs a new {@code CommandException} with the specified detail {@code message} and {@code cause}. - */ - public CommandException(String message, Throwable cause) { - super(message, cause); - } } diff --git a/src/main/java/seedu/address/logic/commands/google/GoogleCommand.java b/src/main/java/seedu/address/logic/commands/google/GoogleCommand.java new file mode 100644 index 000000000000..cdde389f8c11 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/google/GoogleCommand.java @@ -0,0 +1,23 @@ +package seedu.address.logic.commands.google; + +import seedu.address.logic.commands.Command; + +//@@author chivent + +/** + * Shell for Google-type commands + */ +public abstract class GoogleCommand extends Command { + + public static final String COMMAND_WORD = "g"; + public static final String TYPE = ""; + + protected String parameter; + + GoogleCommand(String parameter) { + this.parameter = parameter; + } + GoogleCommand() { + parameter = ""; + } +} diff --git a/src/main/java/seedu/address/logic/commands/google/GoogleDlCommand.java b/src/main/java/seedu/address/logic/commands/google/GoogleDlCommand.java new file mode 100644 index 000000000000..8c7861ffa611 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/google/GoogleDlCommand.java @@ -0,0 +1,99 @@ +package seedu.address.logic.commands.google; + +//@@author chivent + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_CONNECTION_FAILURE; + +import com.google.api.gax.rpc.ApiException; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles download and upload (to be added) of files to Google Photos + */ +public class GoogleDlCommand extends GoogleCommand { + + public static final String MESSAGE_SUCCESS = "%s downloaded into %s"; + public static final String MESSAGE_FAILURE = "%s failed to download." + + " Remember to type a valid name in the right format!"; + public static final String TYPE = "dl"; + public static final String FULL_CMD = COMMAND_WORD + " " + TYPE; + public static final String MESSAGE_USAGE = "Usage of google download (requires an internet connection): " + + "\n- " + FULL_CMD + " /i: " + "Downloads specified image from Google Photos" + + "\n\tExample: " + FULL_CMD + " /i, usage inclusive of <> " + + "\n- " + FULL_CMD + " /a /i: " + "Downloads specified image" + + "from specified album in Google sPhotos" + + "\n\tExample: " + FULL_CMD + " /a /i, usage inclusive of <>" + + "\n- " + FULL_CMD + " /a: " + "Downloads all images from specified album, " + + "takes a longer time depending on number of images" + + "\n\tExample: " + FULL_CMD + " /a, usage inclusive of <> \n\n" + + "!!WARNING: Any files with duplicate naming existing in the folder WILL be replaced"; + + /** + * Index to start from when parsing a image or album name + */ + private static final int START_INDEX = 3; + + public GoogleDlCommand(String parameter) { + super(parameter); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + String org = parameter; + try { + String currDir = model.getCurrDirectory().toString(); + + if (parameter.startsWith("/a")) { + String[] params = parameter.trim().split(" /i<", 2); + if (params.length > 1) { + String albumName = params[0].substring(START_INDEX, params[0].length() - 1); + + // get image name + parameter = params[1].substring(0, params[1].length() - 1); + model.getPhotoHandler(true).downloadAlbumImage(albumName, parameter, currDir); + } else { + parameter = parameter.substring(START_INDEX, parameter.length() - 1); + model.getPhotoHandler(true).downloadWholeAlbum(parameter, currDir); + } + + } else if (parameter.startsWith("/i")) { + parameter = parameter.substring(START_INDEX, parameter.length() - 1); + model.getPhotoHandler(true).downloadImage(parameter, currDir); + } else { + throw new Exception(parameter); + } + + model.updateEntireImageList(); + + } catch (ApiException api) { + throw new CommandException(MESSAGE_CONNECTION_FAILURE); + } catch (CommandException coEx) { + throw coEx; + } catch (Exception ex) { + if (parameter.isEmpty()) { + parameter = org; + } + throw new CommandException(String.format(MESSAGE_FAILURE, parameter) + "\n\n" + MESSAGE_USAGE); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, parameter, model.getCurrDirectory().toString())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof GoogleDlCommand)) { //this handles null as well. + return false; + } + return ((GoogleDlCommand) other).parameter.equals(this.parameter); + } +} diff --git a/src/main/java/seedu/address/logic/commands/google/GoogleLsCommand.java b/src/main/java/seedu/address/logic/commands/google/GoogleLsCommand.java new file mode 100644 index 000000000000..7b360282ed22 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/google/GoogleLsCommand.java @@ -0,0 +1,84 @@ +package seedu.address.logic.commands.google; + +//@@author chivent + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_CONNECTION_FAILURE; + +import java.util.List; + +import com.google.api.gax.rpc.ApiException; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles listing of files from Google Photos + */ +public class GoogleLsCommand extends GoogleCommand { + + public static final String FAILURE_MESSAGE = "Failed to list"; + public static final String TYPE = "ls"; + public static final String FULL_CMD = COMMAND_WORD + " " + TYPE; + public static final String MESSAGE_USAGE = "Usage of google list (requires an internet connection): " + + "\n- " + FULL_CMD + " Lists all photos in Google Photos, " + + "takes a longer amount of time depending on number of images in Google Photos." + + "\n- " + FULL_CMD + " /a: " + "Lists all albums in Google Photos" + + "\n- " + FULL_CMD + " : " + "Lists all photos in specified album from Google Photos" + + "\n\tExample: " + FULL_CMD + " , usage inclusive of <>"; + + public GoogleLsCommand(String parameter) { + super(parameter); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) + throws CommandException { + requireNonNull(model); + + List printTarget; + StringBuilder toPrint = new StringBuilder(); + try { + if (parameter.isEmpty()) { + printTarget = model.getPhotoHandler(true).returnAllImagesList(); + + //Retrieve all names and call + } else if ("/a".equals(parameter)) { + printTarget = model.getPhotoHandler(true).returnAllAlbumsList(); + } else { + parameter = parameter.substring(1, parameter.length() - 1); + printTarget = model.getPhotoHandler(true).returnAllImagesinAlbum(parameter); + } + + for (String name : printTarget) { + toPrint.append(name + " \n"); + } + + if (toPrint.toString().isEmpty() && "/a".equals(parameter)) { + toPrint.append("Empty! No images to be displayed"); + } + + } catch (ApiException api) { + throw new CommandException(MESSAGE_CONNECTION_FAILURE + "\n\n" + MESSAGE_USAGE); + } catch (CommandException coEx) { + throw coEx; + } catch (Exception ex) { + throw new CommandException(FAILURE_MESSAGE + "\n\n" + MESSAGE_USAGE); + } + + return new CommandResult(toPrint.toString()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof GoogleLsCommand)) { //this handles null as well. + return false; + } + return ((GoogleLsCommand) other).parameter.equals(this.parameter); + } +} diff --git a/src/main/java/seedu/address/logic/commands/google/GoogleRefreshCommand.java b/src/main/java/seedu/address/logic/commands/google/GoogleRefreshCommand.java new file mode 100644 index 000000000000..41910666d127 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/google/GoogleRefreshCommand.java @@ -0,0 +1,41 @@ +package seedu.address.logic.commands.google; + +//@@author chivent + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_CONNECTION_FAILURE; + +import com.google.api.gax.rpc.ApiException; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Refreshes retrieved albums and images from google + */ +public class GoogleRefreshCommand extends GoogleCommand { + public static final String FAILURE_MESSAGE = "Failed to refresh"; + public static final String TYPE = "refresh"; + public static final String FULL_CMD = COMMAND_WORD + " " + TYPE; + public static final String MESSAGE_USAGE = "Usage of google refresh (requires an internet connection): " + + "\n- " + FULL_CMD + ": Refreshes image and album list gotten from google "; + + @Override + public CommandResult execute(Model model, CommandHistory history) + throws CommandException { + try { + requireNonNull(model); + + model.getPhotoHandler(true).refreshLists(); + } catch (ApiException api) { + throw new CommandException(MESSAGE_CONNECTION_FAILURE + "\n\n" + MESSAGE_USAGE); + } catch (CommandException coEx) { + throw coEx; + } catch (Exception ex) { + throw new CommandException(FAILURE_MESSAGE + "\n\n" + MESSAGE_USAGE); + } + return new CommandResult("Images and albums refreshed!"); + } +} diff --git a/src/main/java/seedu/address/logic/commands/google/GoogleUploadCommand.java b/src/main/java/seedu/address/logic/commands/google/GoogleUploadCommand.java new file mode 100644 index 000000000000..a8403b692ab5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/google/GoogleUploadCommand.java @@ -0,0 +1,94 @@ +package seedu.address.logic.commands.google; + +//@@author chivent + +import static java.util.Objects.requireNonNull; + +import com.google.api.gax.rpc.ApiException; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles download and upload (to be added) of files to Google Photos + */ +public class GoogleUploadCommand extends GoogleCommand { + public static final String TYPE = "ul"; + public static final String FULL_CMD = COMMAND_WORD + " " + TYPE; + public static final String MESSAGE_USAGE = "Usage of google upload (requires an internet connection): " + + "\n- " + FULL_CMD + " : Uploads specified image to Google Photos" + + "\n\tExample: " + FULL_CMD + " , usage inclusive of <> " + + "\n- " + FULL_CMD + " all: Uploads all images in current directory to Google Photos, " + + "takes a longer amount of time depending on number of images to upload. \n\n" + + "!!NOTE: All photos uploaded from Piconso can be found in album 'Piconso Uploads'"; + + public static final String MESSAGE_ALL_DUPLICATE = "Failure to upload. %s already exist(s) in Google Photos."; + private static final String ADVICE = "\n\nYou'll need to use `g refresh` before you can see it by ls!"; + public static final String MESSAGE_SUCCESS = "Successfully uploaded to Google Photos: \n%s" + ADVICE; + public static final String MESSAGE_DUPLICATE = "Upload success. Some of the images in the selected folder are " + + "duplicates, only the following were uploaded: \n%s" + ADVICE; + public static final String MESSAGE_FAILURE = "%s failed to upload." + + "Remember to type a valid name in the right format!" + "\n\n" + MESSAGE_USAGE; + + public GoogleUploadCommand(String parameter) { + super(parameter); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + String org = parameter; + String message; + try { + if (parameter.startsWith("all")) { + message = model.getPhotoHandler(true).uploadAll(model.getCurrDirectory().toString()); + return returnUploadMessage(message); + } else { + parameter = parameter.substring(1, parameter.length() - 1); + message = model.getPhotoHandler(true).uploadImage(parameter, model.getCurrDirectory().toString()); + return returnUploadMessage(message); + } + } catch (ApiException ex) { + throw new CommandException(ex.getMessage()); + } catch (CommandException coEx) { + throw coEx; + } catch (Exception ex) { + if (parameter.isEmpty()) { + parameter = org; + } + throw new CommandException(String.format(MESSAGE_FAILURE, parameter) + "\n\n" + MESSAGE_USAGE); + } + } + + /** + * Parses and prepares a message to return to result display for uploading + * @param uploaded duplicate list + * @return result to return to display + */ + public CommandResult returnUploadMessage(String uploaded) { + String message = uploaded; + String allImages = "All images in directory"; + if (uploaded.isEmpty()) { + return new CommandResult(String.format(MESSAGE_ALL_DUPLICATE, allImages)); + } else if (uploaded.substring(0, 4).equals(".all")) { + message = uploaded.substring(4); + return new CommandResult(String.format(MESSAGE_SUCCESS, message)); + } else { + return new CommandResult(String.format(MESSAGE_DUPLICATE, message)); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof GoogleUploadCommand)) { //this handles null as well. + return false; + } + return ((GoogleUploadCommand) other).parameter.equals(this.parameter); + } +} diff --git a/src/main/java/seedu/address/logic/commands/layer/LayerAddCommand.java b/src/main/java/seedu/address/logic/commands/layer/LayerAddCommand.java new file mode 100644 index 000000000000..6240285c20c9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/layer/LayerAddCommand.java @@ -0,0 +1,81 @@ +package seedu.address.logic.commands.layer; + +//@@author j-lum +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Logger; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.PreviewImage; + + +/** + * Handles the repositioning of Layers. + * Commands are in the format - layer add [index]. + * Refer to the open command. + */ + +public class LayerAddCommand extends LayerCommand { + public static final String TYPE = COMMAND_WORD + " add"; + public static final String MESSAGE_USAGE = "Usage of layer add: " + + "\n- " + TYPE + " [INDEX]: " + + "Adds the image identified by the index number in the current batch to a new layer." + + "\n\tExample: " + TYPE + " 2, adds the image with index 2 to the current canvas as the top-most layer."; + + public static final String OUTPUT_SUCCESS = "Layer added!"; + public static final String OUTPUT_FAILURE = "Invalid index provided or initial image not selected!"; + private static final int BATCH_SIZE = 10; + + private static final Logger logger = LogsCenter.getLogger(LayerAddCommand.class); + + + public LayerAddCommand(String args) { + super(args); + } + + @Override + + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + int i; + Index index; + List dirImageList = model.getDirectoryImageList(); + Image img; + + try { + i = Integer.parseInt(args); + index = Index.fromOneBased(i); + if (index.getZeroBased() >= dirImageList.size() || index.getZeroBased() >= BATCH_SIZE) { + throw new NumberFormatException(Messages.MESSAGE_INDEX_END_OF_IMAGE_LIST); + } + index = Index.fromOneBased(i); + } catch (NumberFormatException e) { + throw new CommandException(OUTPUT_FAILURE); + } + + Path selectedImagePath = dirImageList.get(index.getZeroBased()); + + try { + String selectedImage = selectedImagePath.toString(); + FileInputStream fis = new FileInputStream(selectedImage); + img = new Image(fis); + model.addLayer(new PreviewImage(SwingFXUtils.fromFXImage(img, null))); + } catch (FileNotFoundException e) { + throw new CommandException(OUTPUT_FAILURE); + } + + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + + return new CommandResult(OUTPUT_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/layer/LayerCommand.java b/src/main/java/seedu/address/logic/commands/layer/LayerCommand.java new file mode 100644 index 000000000000..9309a4bc88ee --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/layer/LayerCommand.java @@ -0,0 +1,18 @@ +package seedu.address.logic.commands.layer; + +import seedu.address.logic.commands.Command; + +//@@author j-lum + +/** + * Parent for all Layer commands + */ +public abstract class LayerCommand extends Command { + + public static final String COMMAND_WORD = "layer"; + protected String args; + + LayerCommand(String args) { + this.args = args; + } +} diff --git a/src/main/java/seedu/address/logic/commands/layer/LayerDeleteCommand.java b/src/main/java/seedu/address/logic/commands/layer/LayerDeleteCommand.java new file mode 100644 index 000000000000..7330b49ac4a8 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/layer/LayerDeleteCommand.java @@ -0,0 +1,61 @@ +package seedu.address.logic.commands.layer; + +//@@author j-lum +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles the deleting of layers. + * Commands are in the format - layer delete [index]. + * Invalid indexes, locked layers are handled. + * Layer operations cannot be undone! + */ + +public class LayerDeleteCommand extends LayerCommand { + public static final String TYPE = COMMAND_WORD + " delete"; + public static final String MESSAGE_USAGE = "Usage of layer delete: " + + "\n- " + TYPE + " [INDEX]: " + "Deletes the layer " + + "\n\tExample: " + TYPE + " 2, deletes the 2nd layer in the canvas."; + + public static final String OUTPUT_SUCCESS = "Layer deleted! Now working on layer index: %d."; + public static final String OUTPUT_FAILURE = "Invalid layer index provided!"; + + private static final Logger logger = LogsCenter.getLogger(LayerDeleteCommand.class); + + + public LayerDeleteCommand(String args) { + super(args); + } + + @Override + + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (args == null) { + throw new CommandException(OUTPUT_FAILURE); + } + int index; + Index toRemove; + Index currentLayer; + try { + index = Integer.parseInt(args); + toRemove = Index.fromOneBased(index); + currentLayer = model.removeLayer(toRemove); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + throw new CommandException(OUTPUT_FAILURE); + } catch (IllegalOperationException e) { + throw new CommandException(e.getMessage()); + } + + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + + return new CommandResult(String.format(OUTPUT_SUCCESS, currentLayer.getOneBased())); + } +} diff --git a/src/main/java/seedu/address/logic/commands/layer/LayerPositionCommand.java b/src/main/java/seedu/address/logic/commands/layer/LayerPositionCommand.java new file mode 100644 index 000000000000..cf55c53f8276 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/layer/LayerPositionCommand.java @@ -0,0 +1,62 @@ +package seedu.address.logic.commands.layer; + +//@@author j-lum +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles the repositioning of Layers. + * Commands are in the form - layer position [x_positionxy_position] + * Invalid arguments are handled. + * The top left of the canvas is considered to be (0, 0). + */ + +public class LayerPositionCommand extends LayerCommand { + public static final String TYPE = COMMAND_WORD + " position"; + public static final String MESSAGE_USAGE = "Usage of layer position: " + + "\n- " + TYPE + " [POSITION]: " + "Repositions the layer to the provided co-ordinates" + + "\n\tExample: " + TYPE + " 50x100, sets the layer's top right corner " + + "to be 50px to the right and 100px from the top"; + + public static final String OUTPUT_SUCCESS = "Layer position is now %d by %d."; + public static final String OUTPUT_FAILURE = "Invalid position provided!"; + + private static final Logger logger = LogsCenter.getLogger(LayerPositionCommand.class); + + + public LayerPositionCommand(String args) { + super(args); + } + + @Override + + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (args == null) { + throw new CommandException(OUTPUT_FAILURE); + } + + String[] argumentArray = args.trim().split("x", 2); + int newX; + int newY; + + try { + newX = Integer.parseInt(argumentArray[0]); + newY = Integer.parseInt((argumentArray.length > 1) ? argumentArray[1] : ""); + } catch (NumberFormatException e) { + throw new CommandException(OUTPUT_FAILURE); + } + model.setCurrentLayerPosition(newX, newY); + + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + + return new CommandResult(String.format(OUTPUT_SUCCESS, + model.getCanvas().getCurrentLayer().getX(), + model.getCanvas().getCurrentLayer().getY())); + } +} diff --git a/src/main/java/seedu/address/logic/commands/layer/LayerSelectCommand.java b/src/main/java/seedu/address/logic/commands/layer/LayerSelectCommand.java new file mode 100644 index 000000000000..f74aa6a1f19e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/layer/LayerSelectCommand.java @@ -0,0 +1,52 @@ +package seedu.address.logic.commands.layer; + +//@@author j-lum +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles selection of the current working layer. + * Commands are in the form : layer select [index]. + * Invalid indexes and missing canvas objects are handled. + * The current working layer is also protected from deletion. + */ + +public class LayerSelectCommand extends LayerCommand { + public static final String TYPE = COMMAND_WORD + " select"; + public static final String MESSAGE_USAGE = "Usage of layer select: " + + "\n- " + TYPE + " [INDEX]: " + "Selects a layer to work on." + + "\n\tExample: " + TYPE + " 2, selects the layer with index 2 to work on."; + + public static final String OUTPUT_SUCCESS = "Now working on layer %d."; + public static final String OUTPUT_FAILURE = "Invalid layer index provided!"; + public static final String OUTPUT_MISSING_CANVAS = "You must open an image file for editing before proceeding!"; + + public LayerSelectCommand(String args) { + super(args); + } + + @Override + + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (args == null) { + throw new CommandException(OUTPUT_FAILURE); + } + int i; + Index index; + try { + i = Integer.parseInt(args); + index = Index.fromOneBased(i); + model.setCurrentLayer(index); + model.refreshHistoryList(); + } catch (NullPointerException e) { + throw new CommandException(OUTPUT_MISSING_CANVAS); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + throw new CommandException(OUTPUT_FAILURE); + } + + return new CommandResult(String.format(OUTPUT_SUCCESS, index.getOneBased())); + } +} diff --git a/src/main/java/seedu/address/logic/commands/layer/LayerSwapCommand.java b/src/main/java/seedu/address/logic/commands/layer/LayerSwapCommand.java new file mode 100644 index 000000000000..d46dad4eaba7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/layer/LayerSwapCommand.java @@ -0,0 +1,67 @@ +package seedu.address.logic.commands.layer; + +//@@author j-lum +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.util.ImageMagickUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Handles the changing of layer order. + * Commands are in the format - layer swap [index index]. + * Invalid indexes and invalid operations are handled. + * The index is also used for the z-index. + * The lower the z-index, the further behind it will be during image composition. + */ + +public class LayerSwapCommand extends LayerCommand { + public static final String TYPE = COMMAND_WORD + " swap"; + public static final String MESSAGE_USAGE = "Usage of layer swap: " + + "\n- " + TYPE + " [TO] [FROM]: " + "Swaps the order of two distinct layers" + + "\n\tExample: " + TYPE + " 1 3, swaps the order of the 1st and 3rd layer in the canvas."; + + public static final String OUTPUT_SUCCESS = "Layers %d and %d are now swapped."; + public static final String OUTPUT_FAILURE = "Invalid index(es) provided!"; + public static final String OUTPUT_ILLEGAL = "Unable to swap layers!"; + + private static final Logger logger = LogsCenter.getLogger(LayerSwapCommand.class); + + + public LayerSwapCommand(String args) { + super(args); + } + + @Override + + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + String[] argumentArray = args.trim().split(" ", 2); + + int to; + int from; + Index toIndex; + Index fromIndex; + + try { + to = Integer.parseInt(argumentArray[0]); + from = Integer.parseInt((argumentArray.length > 1) ? argumentArray[1] : ""); + toIndex = Index.fromOneBased(to); + fromIndex = Index.fromOneBased(from); + model.swapLayer(toIndex, fromIndex); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + throw new CommandException(OUTPUT_FAILURE); + } catch (IllegalOperationException e) { + throw new CommandException(OUTPUT_ILLEGAL); + } + + ImageMagickUtil.render(model.getCanvas(), logger, "preview"); + + return new CommandResult(String.format(OUTPUT_SUCCESS, + toIndex.getOneBased(), fromIndex.getOneBased())); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e83..000000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index b7d57f5db86a..000000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,92 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.HistoryCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.commands.RedoCommand; -import seedu.address.logic.commands.SelectCommand; -import seedu.address.logic.commands.UndoCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case SelectCommand.COMMAND_WORD: - return new SelectCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case HistoryCommand.COMMAND_WORD: - return new HistoryCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - case UndoCommand.COMMAND_WORD: - return new UndoCommand(); - - case RedoCommand.COMMAND_WORD: - return new RedoCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ApplyCommandParser.java b/src/main/java/seedu/address/logic/parser/ApplyCommandParser.java new file mode 100644 index 000000000000..11859d9262ae --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ApplyCommandParser.java @@ -0,0 +1,42 @@ +package seedu.address.logic.parser; + +//@@author lancelotwilow +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.stream.Stream; + +import seedu.address.logic.commands.ApplyCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.transformation.Transformation; + +/** + * Parses input arguments and creates a new ExampleCommand object + */ +public class ApplyCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the ExampleCommand + * and returns an ExampleCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ApplyCommand parse(String args) throws ParseException { + String[] all = Stream.of(args.split(" ")) + .filter(x -> !"".equals(x)) + .toArray(String[]::new); + if (all.length < 1) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ApplyCommand.MESSAGE_USAGE)); + } + if (all[0].equals("raw")) { + if (all.length < 2) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ApplyCommand.MESSAGE_USAGE_RAW)); + } + return new ApplyCommand(Arrays.copyOfRange(all, 1, all.length)); + } + String operation = all[0]; + String[] cmds = Arrays.copyOfRange(all, 1, all.length); + return new ApplyCommand(new Transformation(operation, cmds)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa4888..8f6f60bf92be 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -136,13 +136,12 @@ private static class PrefixPosition { this.startPosition = startPosition; } - int getStartPosition() { + protected int getStartPosition() { return startPosition; } - Prefix getPrefix() { + protected Prefix getPrefix() { return prefix; } } - } diff --git a/src/main/java/seedu/address/logic/parser/CanvasCommandParser.java b/src/main/java/seedu/address/logic/parser/CanvasCommandParser.java new file mode 100644 index 000000000000..f0a303b7c63a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CanvasCommandParser.java @@ -0,0 +1,49 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.ENTIRE_CANVAS_MESSAGE; + +import seedu.address.logic.commands.canvas.CanvasAutoResizeCommand; +import seedu.address.logic.commands.canvas.CanvasBgcolorCommand; +import seedu.address.logic.commands.canvas.CanvasCommand; +import seedu.address.logic.commands.canvas.CanvasSizeCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author j-lum + +/** + * Parses input arguments and creates the corresponding Google type command object + */ +public class CanvasCommandParser { + /** + * Parses the sub-command and arguments for the Canvas command + * + * @throws ParseException if the user input does not conform an expected format + */ + public CanvasCommand parse(String args) throws ParseException { + requireNonNull(args); + String[] argumentArray = args.trim().split(" ", 2); + String subcommand = argumentArray[0]; + String argument = (argumentArray.length > 1) ? argumentArray[1] : null; + + switch (subcommand) { + case "auto-resize": { + if (argument == null) { + throw new ParseException(CanvasAutoResizeCommand.MESSAGE_USAGE); + } + return new CanvasAutoResizeCommand(argument); + } + case "bgcolor": { + if (argument == null) { + throw new ParseException(CanvasBgcolorCommand.MESSAGE_USAGE); + } + return new CanvasBgcolorCommand(argument); + } + case "size": { + return new CanvasSizeCommand(argument); + } + default: + throw new ParseException(ENTIRE_CANVAS_MESSAGE); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/CdCommandParser.java similarity index 50% rename from src/main/java/seedu/address/logic/parser/DeleteCommandParser.java rename to src/main/java/seedu/address/logic/parser/CdCommandParser.java index 4d1f4bb0e4ec..71de42a47146 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/CdCommandParser.java @@ -1,29 +1,30 @@ +//@@author benedictcss package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; +import java.nio.file.Path; + +import seedu.address.logic.commands.CdCommand; import seedu.address.logic.parser.exceptions.ParseException; /** - * Parses input arguments and creates a new DeleteCommand object + * Parses input arguments and creates a new CdCommand object */ -public class DeleteCommandParser implements Parser { +public class CdCommandParser { /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns an DeleteCommand object for execution. + * Parses the given {@code String} of arguments in the context of the CdCommand + * and returns an CdCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public DeleteCommand parse(String args) throws ParseException { + public CdCommand parse(String args) throws ParseException { try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + Path dir = ParserUtil.parseFilePath(args); + return new CdCommand(dir); } catch (ParseException pe) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CdCommand.MESSAGE_USAGE), pe); } } - } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java deleted file mode 100644 index 75b1a9bf1190..000000000000 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.parser; - -/** - * Contains Command Line Interface (CLI) syntax definitions common to multiple commands - */ -public class CliSyntax { - - /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); - -} diff --git a/src/main/java/seedu/address/logic/parser/CreateApplyCommandParser.java b/src/main/java/seedu/address/logic/parser/CreateApplyCommandParser.java new file mode 100644 index 000000000000..d4d7fdb1e78f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CreateApplyCommandParser.java @@ -0,0 +1,38 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.logic.commands.CreateApplyCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.transformation.Transformation; + +/** + * . + */ +public class CreateApplyCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ExampleCommand + * and returns an ExampleCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CreateApplyCommand parse(String args) throws ParseException { + String[] all = args.split(" "); + List transformations = new ArrayList<>(); + if (all.length < 3 || all[1].contains("|")) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, CreateApplyCommand.MESSAGE_USAGE)); + } + for (int i = 2; i < all.length; i++) { + String command = all[i]; + String[] cmds = command.split("\\|"); + String name = cmds[0]; + cmds = Arrays.copyOfRange(cmds, 1, cmds.length); + transformations.add(new Transformation(name, cmds)); + } + return new CreateApplyCommand(all[1], transformations); + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java deleted file mode 100644 index 845644b7dea1..000000000000 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new EditCommand object - */ -public class EditCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public EditCommand parse(String args) throws ParseException { - requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - return new EditCommand(index, editPersonDescriptor); - } - - /** - * 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)); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index b186a967cb94..000000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns an FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/GoogleCommandParser.java b/src/main/java/seedu/address/logic/parser/GoogleCommandParser.java new file mode 100644 index 000000000000..a6a41c76437d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/GoogleCommandParser.java @@ -0,0 +1,83 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.ENTIRE_GOOGLE_MESSAGE; +import static seedu.address.commons.core.Messages.MESSAGE_GOOGLE_INVALID_FORMAT; + +import seedu.address.logic.commands.google.GoogleCommand; +import seedu.address.logic.commands.google.GoogleDlCommand; +import seedu.address.logic.commands.google.GoogleLsCommand; +import seedu.address.logic.commands.google.GoogleRefreshCommand; +import seedu.address.logic.commands.google.GoogleUploadCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author chivent + +/** + * Parses input arguments and creates the corresponding Google type command object + */ +public class GoogleCommandParser { + + private String type = ""; + + /** + * Parses the given {@code String} of arguments in the context of the GoogleCommand + * and returns an GoogleCommand object for execution. + * + * @throws ParseException if the user input does not conform an expected format + */ + public GoogleCommand parse(String args) throws ParseException { + requireNonNull(args); + + String commandParam = parseArgumentString(args); + + switch (type) { + case "ls": { + return new GoogleLsCommand(commandParam); + } + case "dl": { + if (commandParam.isEmpty()) { + throw new ParseException(MESSAGE_GOOGLE_INVALID_FORMAT + "\n\n" + GoogleDlCommand.MESSAGE_USAGE); + } + return new GoogleDlCommand(commandParam); + } + case "ul": { + if (commandParam.isEmpty()) { + throw new ParseException(MESSAGE_GOOGLE_INVALID_FORMAT + "\n\n" + GoogleUploadCommand.MESSAGE_USAGE); + } + return new GoogleUploadCommand(commandParam); + } + case "refresh": { + return new GoogleRefreshCommand(); + } + default: + throw new ParseException(ENTIRE_GOOGLE_MESSAGE); + } + } + + /** + * Parse argument string to extract type of command and arguments + * + * @param args command string entered by user + * @return parameter to be used in Google-type Command + * @throws ParseException if command format is wrong + */ + private String parseArgumentString(String args) throws ParseException { + + String[] parameters = args.trim().split(" ", 2); + + // checking if argument string is a valid format + if (parameters.length > 0) { + type = parameters[0]; + + // if command has extra arguments + if (parameters.length > 1) { + return parameters[1].trim(); + } else { + return ""; + } + } else { + throw new ParseException(ENTIRE_GOOGLE_MESSAGE); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/LayerCommandParser.java b/src/main/java/seedu/address/logic/parser/LayerCommandParser.java new file mode 100644 index 000000000000..778a6b233d20 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/LayerCommandParser.java @@ -0,0 +1,67 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.ENTIRE_LAYER_MESSAGE; + +import seedu.address.logic.commands.layer.LayerAddCommand; +import seedu.address.logic.commands.layer.LayerCommand; +import seedu.address.logic.commands.layer.LayerDeleteCommand; +import seedu.address.logic.commands.layer.LayerPositionCommand; +import seedu.address.logic.commands.layer.LayerSelectCommand; +import seedu.address.logic.commands.layer.LayerSwapCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + + + +//@@author j-lum + +/** + * Parses input arguments and creates the corresponding Google type command object + */ +public class LayerCommandParser { + + /** + * Parses the sub-command and arguments for the Canvas command + * + * @throws ParseException if the user input does not conform an expected format + */ + public LayerCommand parse(String args) throws ParseException { + requireNonNull(args); + String[] argumentArray = args.trim().split(" ", 2); + String subcommand = argumentArray[0]; + String argument = (argumentArray.length > 1) ? argumentArray[1] : null; + + switch (subcommand) { + case "add": { + if (argument == null) { + throw new ParseException(LayerAddCommand.MESSAGE_USAGE); + } + return new LayerAddCommand(argument); + } + case "delete": { + if (argument == null) { + throw new ParseException(LayerDeleteCommand.MESSAGE_USAGE); + } + return new LayerDeleteCommand(argument); + } + case "select": { + if (argument == null) { + throw new ParseException(LayerSelectCommand.MESSAGE_USAGE); + } + return new LayerSelectCommand(argument); + } + case "position": { + return new LayerPositionCommand(argument); + } + case "swap": { + if (argument == null) { + throw new ParseException(LayerSwapCommand.MESSAGE_USAGE); + } + return new LayerSwapCommand(argument); + } + default: + throw new ParseException(ENTIRE_LAYER_MESSAGE); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java b/src/main/java/seedu/address/logic/parser/OpenCommandParser.java similarity index 60% rename from src/main/java/seedu/address/logic/parser/SelectCommandParser.java rename to src/main/java/seedu/address/logic/parser/OpenCommandParser.java index 565b7f04bfe1..bc1a7ebc4b7f 100644 --- a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/OpenCommandParser.java @@ -3,26 +3,26 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.OpenCommand; import seedu.address.logic.parser.exceptions.ParseException; /** - * Parses input arguments and creates a new SelectCommand object + * Parses input arguments and creates a new OpenCommand object */ -public class SelectCommandParser implements Parser { +public class OpenCommandParser implements Parser { /** - * Parses the given {@code String} of arguments in the context of the SelectCommand - * and returns an SelectCommand object for execution. + * Parses the given {@code String} of arguments in the context of the OpenCommand + * and returns an OpenCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public SelectCommand parse(String args) throws ParseException { + public OpenCommand parse(String args) throws ParseException { try { Index index = ParserUtil.parseIndex(args); - return new SelectCommand(index); + return new OpenCommand(index); } catch (ParseException pe) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, OpenCommand.MESSAGE_USAGE), pe); } } } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 76daf40807e2..fa0cd002c626 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,18 +2,13 @@ import static java.util.Objects.requireNonNull; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -35,90 +30,42 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_NAME_CONSTRAINTS); - } - return new Name(trimmedName); - } + //TODO: Left as an example, to be deleted/replaced. + // /** + // * Parses a {@code String tag} into a {@code Tag}. + // * Leading and trailing whitespaces will be trimmed. + // * + // * @throws ParseException if the given {@code tag} is invalid. + // */ + // public static Tag parseTag(String tag) throws ParseException { + // requireNonNull(tag); + // String trimmedTag = tag.trim(); + // if (!Tag.isValidTagName(trimmedTag)) { + // throw new ParseException(Tag.MESSAGE_TAG_CONSTRAINTS); + // } + // return new Tag(trimmedTag); + // } /** - * Parses a {@code String phone} into a {@code Phone}. + * Parses a {@code String filepath} into a {@code Path}. * Leading and trailing whitespaces will be trimmed. * - * @throws ParseException if the given {@code phone} is invalid. + * @throws ParseException if the given {@code filepath} is invalid. */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_PHONE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } + public static Path parseFilePath(String filepath) throws ParseException { + requireNonNull(filepath); - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_ADDRESS_CONSTRAINTS); + String trimmedFilePath = filepath.trim(); + Path dir; + try { + dir = Paths.get(trimmedFilePath); + } catch (InvalidPathException e) { + throw new ParseException("parsing fail"); } - return new Address(trimmedAddress); - } - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_EMAIL_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_TAG_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + if (dir.toString().equals("")) { + throw new ParseException("Empty"); } - return tagSet; + return dir; } } diff --git a/src/main/java/seedu/address/logic/parser/PiconsoParser.java b/src/main/java/seedu/address/logic/parser/PiconsoParser.java new file mode 100644 index 000000000000..bf7fc8f65e4f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/PiconsoParser.java @@ -0,0 +1,119 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.commands.ApplyCommand; +import seedu.address.logic.commands.CdCommand; +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CreateApplyCommand; +import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.LoginCommand; +import seedu.address.logic.commands.LogoutCommand; +import seedu.address.logic.commands.LsCommand; +import seedu.address.logic.commands.NextCommand; +import seedu.address.logic.commands.OpenCommand; +import seedu.address.logic.commands.PrevCommand; +import seedu.address.logic.commands.RedoAllCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.SaveCommand; +import seedu.address.logic.commands.UndoAllCommand; +import seedu.address.logic.commands.UndoCommand; +import seedu.address.logic.commands.canvas.CanvasCommand; +import seedu.address.logic.commands.google.GoogleCommand; +import seedu.address.logic.commands.layer.LayerCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class PiconsoParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + case CdCommand.COMMAND_WORD: + return new CdCommandParser().parse(arguments); + + case NextCommand.COMMAND_WORD: + return new NextCommand(); + + case PrevCommand.COMMAND_WORD: + return new PrevCommand(); + + case LsCommand.COMMAND_WORD: + return new LsCommand(); + + case OpenCommand.COMMAND_WORD: + return new OpenCommandParser().parse(arguments); + + case ApplyCommand.COMMAND_WORD: + return new ApplyCommandParser().parse(arguments); + + case CreateApplyCommand.COMMAND_WORD: + return new CreateApplyCommandParser().parse(arguments); + + case SaveCommand.COMMAND_WORD: + return new SaveCommandParser().parse(arguments); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + + case UndoAllCommand.COMMAND_WORD: + return new UndoAllCommand(); + + case RedoAllCommand.COMMAND_WORD: + return new RedoAllCommand(); + + case LoginCommand.COMMAND_WORD: + return new LoginCommand(); + + case LogoutCommand.COMMAND_WORD: + return new LogoutCommand(); + + case GoogleCommand.COMMAND_WORD: + return new GoogleCommandParser().parse(arguments); + + case LayerCommand.COMMAND_WORD: + return new LayerCommandParser().parse(arguments); + + case CanvasCommand.COMMAND_WORD: + return new CanvasCommandParser().parse(arguments); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/SaveCommandParser.java b/src/main/java/seedu/address/logic/parser/SaveCommandParser.java new file mode 100644 index 000000000000..5ebf7b14c6e9 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SaveCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.ArrayList; +import java.util.Arrays; + +import seedu.address.logic.commands.SaveCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * . + */ +public class SaveCommandParser implements Parser { + + private ArrayList formats = new ArrayList<>( + Arrays.asList("jpg", "JPG", "jpeg", "JPEG", "tiff", "TIFF", "gif", "GIF", "png", "PNG")); + + /** + * Parses the given {@code String} of arguments in the context of the ExampleCommand + * and returns an ExampleCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SaveCommand parse(String args) throws ParseException { + String[] all = args.split(" "); + if (all.length == 1 && args.trim().equals("")) { + return new SaveCommand(); + } + if (all.length != 2 || isFormatValid(all[1])) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SaveCommand.MESSAGE_USAGE)); + } + return new SaveCommand(all[1]); + } + + private boolean isFormatValid(String fileName) { + String[] parts = fileName.split("\\."); + return parts.length != 2 || !formats.contains(parts[1]); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 7f85c8b9258b..000000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - - /* - * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void updatePerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index ac4521f33199..ff0c1a338a67 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,78 +1,187 @@ package seedu.address.model; -import java.util.function.Predicate; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; +import java.awt.image.BufferedImage; +import java.nio.file.Path; +import java.util.List; + +import javafx.scene.image.Image; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.canvas.Canvas; +import seedu.address.model.google.PhotoHandler; +import seedu.address.model.transformation.Transformation; /** * The API of the Model component. */ public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** + * Get PhotoHandler, directs user to login if yet to be logged in. + * @param subCommand returns true if from an instance such as g ls, dl, ul or refresh + */ + PhotoHandler getPhotoHandler(boolean subCommand) throws CommandException; + + /** + * Set PhotoHandler. + */ + void setPhotoHandler(PhotoHandler instance); + + /** + * Gets currently logged in user + */ + String getUserLoggedIn() throws CommandException; + + /** + * Performs necessary set up for current model (test or actual) + */ + void setUpForGoogle(boolean isTest); + + /** + * Returns true if the current layer's PreviewImage has undone states to restore. + */ + boolean canUndoPreviewImage(); + + /** + * Returns true if the current layer's PreviewImage states to restore. + */ + boolean canRedoPreviewImage(); + + /** + * Undoes one state in the current layer's previewImage. + */ + void undoPreviewImage(); + + /** + * Redoes one state in the current layer's previewImage. + */ + void redoPreviewImage(); + + /** + * Undoes all states in the current layer's previewImage. + */ + void undoAllPreviewImage(); + + /** + * Redoes all states in the current layer's previewImage. + */ + void redoAllPreviewImage(); + + /** + * Updates the userPrefs current directory. + */ + void updateCurrDirectory(Path newCurrDirectory); + + /** + * Retrieves the userPrefs current directory. + */ + Path getCurrDirectory(); + + /** + * Retrieves the list of images in current directory. + */ + List getDirectoryImageList(); - /** Clears existing backing model and replaces with the provided new data. */ - void resetData(ReadOnlyAddressBook newData); + /** + * Returns the total number of images in current directory + */ + int getTotalImagesInDir(); - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); + /** + * Returns the current batch pointer + */ + int numOfRemainingImagesInDir(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns the current batch pointer in {@code UserPrefs} */ - boolean hasPerson(Person person); + int getCurrBatchPointer(); /** - * Deletes the given person. - * The person must exist in the address book. + * Updates entire image list. */ - void deletePerson(Person target); + void updateEntireImageList(); /** - * Adds the given person. - * {@code person} must not already exist in the address book. + * Updates the batch pointer to the next 10 images. */ - void addPerson(Person person); + void updateImageListNextBatch(); /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. + * Updates the batch pointer to the previous 10 images. */ - void updatePerson(Person target, Person editedPerson); + void updateImageListPrevBatch(); - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); + /** + * Retrieves the current displayed original image. + */ + Path getCurrentOriginalImage(); /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. + * Set the current displayed original image. */ - void updateFilteredPersonList(Predicate predicate); + void setCurrentOriginalImage(Path path); /** - * Returns true if the model has previous address book states to restore. + * Retrieves the current displayed preview image. */ - boolean canUndoAddressBook(); + PreviewImage getCurrentPreviewImage(); /** - * Returns true if the model has undone address book states to restore. + * Retrieves the current displayed preview image. */ - boolean canRedoAddressBook(); + Path getCurrentPreviewImagePath(); /** - * Restores the model's address book to its previous state. + * update the preview image stored in the model + * @param image */ - void undoAddressBook(); + void updateCurrentPreviewImage(BufferedImage image); /** - * Restores the model's address book to its previously undone state. + * Update the current displayed original image. + * @param img + * @param imgPath */ - void redoAddressBook(); + void updateCurrentOriginalImage(Image img, Path imgPath); /** - * Saves the current address book state for undo/redo. + * Update the current displayed original image for test. + * @param previewImage */ - void commitAddressBook(); + void updateCurrentOriginalImageForTest(PreviewImage previewImage); + + /** + * update the transformationSet of the current image + * @param transformation - transfomation to be added + */ + void addTransformation(Transformation transformation); + + void addLayer(PreviewImage i, String name); + + void addLayer(PreviewImage i); + + Index removeLayer(Index i) throws IllegalOperationException; + + void setCurrentLayer(Index i); + + void swapLayer(Index to, Index from) throws IllegalOperationException; + + void setCanvasSize(int height, int width); + + void setBackgroundColor(String color); + + void setCanvasAuto(boolean auto); + + int getCanvasHeight(); + + int getCanvasWidth(); + + Canvas getCanvas(); + + //void saveCanvas(String fileName) throws IOException, InterruptedException, UnsupportedPlatformException; + + void refreshHistoryList(); + + void setCurrentLayerPosition(int newX, int newY); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index a664602ef5b1..5f0764633045 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -1,18 +1,33 @@ package seedu.address.model; -import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_CONNECTION_FAILURE; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.google.PhotosLibraryClientFactory.BLOCKER; +import static seedu.address.model.google.PhotosLibraryClientFactory.DATA_STORE; +import static seedu.address.model.google.PhotosLibraryClientFactory.TEST_FILE; -import java.util.function.Predicate; +import java.awt.image.BufferedImage; +import java.nio.file.Path; +import java.util.List; import java.util.logging.Logger; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; import seedu.address.commons.core.ComponentManager; +import seedu.address.commons.core.EventsCenter; import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.model.person.Person; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.ChangeDirectoryEvent; +import seedu.address.commons.events.ui.HistoryUpdateEvent; +import seedu.address.commons.events.ui.LayerUpdateEvent; +import seedu.address.commons.events.ui.UpdateFilmReelEvent; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.commons.util.FileUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.canvas.Canvas; +import seedu.address.model.google.PhotoHandler; +import seedu.address.model.google.PhotosLibraryClientFactory; +import seedu.address.model.transformation.Transformation; /** * Represents the in-memory model of the address book data. @@ -20,113 +35,383 @@ public class ModelManager extends ComponentManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - private final VersionedAddressBook versionedAddressBook; - private final FilteredList filteredPersons; + private List dirImageList; + private Path currentOriginalImage; + private PhotoHandler photoLibrary; + private Canvas canvas; + + private final UserPrefs userPrefs; /** - * Initializes a ModelManager with the given addressBook and userPrefs. + * Strictly for test mode. Initializes a ModelManager with the given addressBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { + public ModelManager(UserPrefs userPrefs, boolean isTest) { super(); - requireAllNonNull(addressBook, userPrefs); + requireAllNonNull(userPrefs); + + logger.fine("Initializing with user prefs " + userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); + this.userPrefs = userPrefs; + this.userPrefs.initImageList(); + dirImageList = this.userPrefs.getCurrImageListBatch(); - versionedAddressBook = new VersionedAddressBook(addressBook); - filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + setUpForGoogle(isTest); } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new UserPrefs(), true); } + //=========== Directory Image List Accessors ============================================================= + // @@author benedictcss + /** + * Returns an array list of the images from the current directory {@code dirImageList} + * backed by the list of {@code userPrefs} + */ @Override - public void resetData(ReadOnlyAddressBook newData) { - versionedAddressBook.resetData(newData); - indicateAddressBookChanged(); + public List getDirectoryImageList() { + this.dirImageList = userPrefs.getCurrImageListBatch(); + return this.dirImageList; } + /** + * Returns the total number of images in current directory + */ @Override - public ReadOnlyAddressBook getAddressBook() { - return versionedAddressBook; + public int getTotalImagesInDir() { + return userPrefs.getTotalImagesInDir(); } - /** Raises an event to indicate the model has changed */ - private void indicateAddressBookChanged() { - raise(new AddressBookChangedEvent(versionedAddressBook)); + /** + * Returns the current number of remaining pictures in {@code UserPrefs} + */ + @Override + public int numOfRemainingImagesInDir() { + return userPrefs.numOfRemainingImagesInDir(); } + /** + * Returns the current batch pointer in {@code UserPrefs} + */ @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return versionedAddressBook.hasPerson(person); + public int getCurrBatchPointer() { + return userPrefs.getCurrBatchPointer(); } + /** + * Update entire image list in {@code UserPrefs} + */ @Override - public void deletePerson(Person target) { - versionedAddressBook.removePerson(target); - indicateAddressBookChanged(); + public void updateEntireImageList() { + userPrefs.initImageList(); + EventsCenter.getInstance().post(new UpdateFilmReelEvent(getDirectoryImageList())); } + /** + * Updates the batch pointer in {@code UserPrefs} + */ @Override - public void addPerson(Person person) { - versionedAddressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - indicateAddressBookChanged(); + public void updateImageListNextBatch() { + userPrefs.updateImageListNextBatch(); + EventsCenter.getInstance().post(new UpdateFilmReelEvent(getDirectoryImageList())); + } + + /** + * Updates the batch pointer in {@code UserPrefs} + */ + public void updateImageListPrevBatch() { + userPrefs.updateImageListPrevBatch(); + EventsCenter.getInstance().post(new UpdateFilmReelEvent(getDirectoryImageList())); } @Override - public void updatePerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public Path getCurrentOriginalImage() { + return this.currentOriginalImage; + } - versionedAddressBook.updatePerson(target, editedPerson); - indicateAddressBookChanged(); + @Override + public void setCurrentOriginalImage(Path path) { + this.currentOriginalImage = path; } - //=========== Filtered Person List Accessors ============================================================= + /** + * Update the current displayed original image and + * reinitialize the previewImageManager with the new image + */ + @Override + public void updateCurrentOriginalImage(Image img, Path imgPath) { + currentOriginalImage = imgPath; + PreviewImage selectedImage = new PreviewImage(SwingFXUtils.fromFXImage(img, null)); + canvas = new Canvas(selectedImage); + + refreshHistoryList(); + refreshLayerList(); + } /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} + * Update the current displayed original image and + * reinitialize the previewImageManager with the new image, without imgPath */ @Override - public ObservableList getFilteredPersonList() { - return FXCollections.unmodifiableObservableList(filteredPersons); + public void updateCurrentOriginalImageForTest(PreviewImage previewImage) { + canvas = new Canvas(previewImage); + } + //@@author + + //=========== GoogleClient Accessors ============================================================= + // @@author chivent + @Override + public void setPhotoHandler(PhotoHandler instance) { + photoLibrary = instance; } @Override - public void updateFilteredPersonList(Predicate predicate) { - requireNonNull(predicate); - filteredPersons.setPredicate(predicate); + public PhotoHandler getPhotoHandler(boolean subCommand) throws CommandException { + if (photoLibrary == null) { + if (subCommand) { + throw new CommandException("You are not logged in! Please login with `login` to proceed."); + } + try { + photoLibrary = PhotosLibraryClientFactory.createClient(); + if (photoLibrary == null) { + throw new CommandException(MESSAGE_CONNECTION_FAILURE); + } + } catch (Exception e) { + throw new CommandException(MESSAGE_CONNECTION_FAILURE); + } + } + return photoLibrary; + } + + @Override + public String getUserLoggedIn () throws CommandException { + if (photoLibrary == null) { + return null; + } + return getPhotoHandler(false).identifyUser(); + } + + @Override + public void setUpForGoogle(boolean isTest) { + if (isTest) { + FileUtil.createDirectoriesIfMissing(DATA_STORE); + try { + FileUtil.createIfMissing(TEST_FILE); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + FileUtil.deleteIfAvaliable(TEST_FILE); + if (!BLOCKER.exists()) { + try { + photoLibrary = PhotosLibraryClientFactory.loginUserIfPossible(); + } catch (Exception e) { + logger.warning("Unable to log into user account"); + } + } + } } //=========== Undo/Redo ================================================================================= + // @@author ihwk1996 + @Override + public boolean canUndoPreviewImage() { + return getCurrentPreviewImage().canUndo(); + } @Override - public boolean canUndoAddressBook() { - return versionedAddressBook.canUndo(); + public boolean canRedoPreviewImage() { + return getCurrentPreviewImage().canRedo(); } @Override - public boolean canRedoAddressBook() { - return versionedAddressBook.canRedo(); + public void undoPreviewImage() { + getCurrentPreviewImage().undo(); + refreshHistoryList(); } @Override - public void undoAddressBook() { - versionedAddressBook.undo(); - indicateAddressBookChanged(); + public void redoPreviewImage() { + getCurrentPreviewImage().redo(); + refreshHistoryList(); } @Override - public void redoAddressBook() { - versionedAddressBook.redo(); - indicateAddressBookChanged(); + public void undoAllPreviewImage() { + getCurrentPreviewImage().undoAll(); + refreshHistoryList(); + } + + @Override + public void redoAllPreviewImage() { + getCurrentPreviewImage().redoAll(); + refreshHistoryList(); + } + + //=========== get/updating preview image ========================================================================== + + /** + * Adds a transformation to current layer + * @param transformation transformation to add + */ + public void addTransformation(Transformation transformation) { + canvas.getCurrentLayer().addTransformation(transformation); + refreshHistoryList(); + } + + @Override + public PreviewImage getCurrentPreviewImage() { + return canvas.getCurrentLayer().getImage(); + } + + @Override + public Path getCurrentPreviewImagePath() { + return getCurrentPreviewImage().getCurrentPath(); + } + + //@@author lancelotwillow + @Override + public void updateCurrentPreviewImage(BufferedImage image) { + try { + canvas.getCurrentLayer().getImage().commit(image); + refreshHistoryList(); + } catch (Exception e) { + logger.severe(e.getMessage()); + } } + //=========== Update UserPrefs ========================================================================== + // @@author benedictcss @Override - public void commitAddressBook() { - versionedAddressBook.commit(); + public void updateCurrDirectory(Path newCurrDirectory) { + this.userPrefs.updateUserPrefs(newCurrDirectory); + EventsCenter.getInstance().post(new ChangeDirectoryEvent(getCurrDirectory().toString())); + EventsCenter.getInstance().post(new UpdateFilmReelEvent(getDirectoryImageList())); + } + + @Override + public Path getCurrDirectory() { + return this.userPrefs.getCurrDirectory(); + } + + //=========== Canvas and layers ========================================================================== + + //@@author j-lum + + /** + * Sets the background color of the canvas to the provided color. + * @param color - valid color string in either hex, rgb/hsl(a) or none. + */ + public void setBackgroundColor(String color) { + canvas.setBackgroundColor(color); + } + + /** + * Sets the canvas size to the new size provided. + * @param height - a integer larger than 0. + * @param width - a integer larger than 0. + */ + public void setCanvasSize(int height, int width) { + canvas.setHeight(height); + canvas.setWidth(width); + } + + /** + * Turns auto-resizing of the canvas on/off. + * @param auto - if true, the canvas auto-resizes. + */ + public void setCanvasAuto(boolean auto) { + canvas.setCanvasAuto(auto); + } + + /** + * @return the height of the canvas. + */ + public int getCanvasHeight() { + return canvas.getHeight(); + } + + /** + * @return the width of the canvas. + */ + public int getCanvasWidth() { + return canvas.getWidth(); + } + + /** + * Adds a layer to the canvas, a name is generated automatically. + * @param i - PreviewImage to add. + */ + public void addLayer(PreviewImage i) { + canvas.addLayer(i); + refreshLayerList(); + } + + /** + * Overloads the addLayer function to handle an optional name. + * @param i - PreviewImage to add + * @param name - Name of the new layer. + */ + public void addLayer(PreviewImage i, String name) { + canvas.addLayer(i, name); + refreshLayerList(); + } + + /** + * Removes the layer at the given index. + * @param i - Index of the layer to remove. + * @return the new index of the current layer. + * @throws IllegalOperationException - thrown if the current layer is being removed + * or the only layer is being removed. + */ + + public Index removeLayer(Index i) throws IllegalOperationException { + Index tmp = canvas.removeLayer(i); + refreshLayerList(); + return tmp; + } + + public Canvas getCanvas() { + return canvas; + } + + public void setCurrentLayer(Index i) { + canvas.setCurrentLayer(i); + refreshLayerList(); + } + + public void setCurrentLayerPosition(int x, int y) { + canvas.setCurrentLayerPosition(x, y); + } + + /** + * Swaps two layers. + * @param to - Layer 1 + * @param from - Layer 2. + * @throws IllegalOperationException - thrown when the two layers are the same. + */ + public void swapLayer(Index to, Index from) throws IllegalOperationException { + canvas.swapLayer(to, from); + refreshLayerList(); + } + + /** + * Simple utility function that updates the HistoryListPanel and logs the event. + */ + public void refreshHistoryList() { + EventsCenter.getInstance().post( + new HistoryUpdateEvent( + canvas.getCurrentLayer().getImage().getTransformationsAsString())); + } + + /** + * Simple utility function that updates the LayerListPanel and logs the event. + */ + + private void refreshLayerList() { + EventsCenter.getInstance().post( + new LayerUpdateEvent( + canvas.getLayerNames(), canvas.getCurrentLayerIndex())); } @Override @@ -143,8 +428,7 @@ public boolean equals(Object obj) { // state check ModelManager other = (ModelManager) obj; - return versionedAddressBook.equals(other.versionedAddressBook) - && filteredPersons.equals(other.filteredPersons); + return userPrefs.equals(other.userPrefs); } } diff --git a/src/main/java/seedu/address/model/PreviewImage.java b/src/main/java/seedu/address/model/PreviewImage.java new file mode 100644 index 000000000000..cceb5afd01c5 --- /dev/null +++ b/src/main/java/seedu/address/model/PreviewImage.java @@ -0,0 +1,238 @@ +package seedu.address.model; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.logging.Logger; +import javax.imageio.ImageIO; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.transformation.Transformation; +import seedu.address.model.transformation.TransformationSet; + +//@@author ihwk1996 + +/** + * Wraps the image and transformation set for preview. + */ +public class PreviewImage { + + private static final String CACHE_PATH; + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private final TransformationSet transformationSet; + private int height; + private int width; + private int currentIndex; + private int currentSize; // Number of saved images + private long layerId; + + static { + File cache = new File("cache"); + cache.mkdir(); + CACHE_PATH = cache.getPath(); + } + + public PreviewImage(BufferedImage image) { + this.layerId = System.currentTimeMillis(); + this.currentSize = 0; + this.currentIndex = -1; + this.height = image.getHeight(); + this.width = image.getWidth(); + commit(image); + this.transformationSet = new TransformationSet(); + } + + public PreviewImage(BufferedImage image, TransformationSet transformationSet) { + this.layerId = System.currentTimeMillis(); + this.currentSize = 0; + this.currentIndex = -1; + this.height = image.getHeight(); + this.width = image.getWidth(); + commit(image); + this.transformationSet = transformationSet; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public int getCurrentIndex() { + return currentIndex; + } + + public int getCurrentSize() { + return currentSize; + } + + /** + * Check if have previous states to undo. + */ + public boolean canUndo() { + return currentIndex > 0; + } + + /** + * Check if have previous undone states to redo. + */ + public boolean canRedo() { + return currentIndex < currentSize - 1; + } + + /** + * Decrement current index if able to undo. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentIndex--; + } + + /** + * Increment current index if able to redo. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentIndex++; + } + + /** + * Decrement current index if able to undo. + */ + public void undoAll() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentIndex = 0; + } + + /** + * Increment current index if able to redo. + */ + public void redoAll() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentIndex = currentSize - 1; + } + + /** + * Determine if history needs to be purged before committing. + */ + public void commit(BufferedImage image) { + if (currentIndex == currentSize - 1) { + normalCommit(image); + } else { + purgeAndCommit(image); + } + } + + /** + * Increment size and current index, then cache the image. + */ + private void normalCommit(BufferedImage image) { + try { + currentSize++; + currentIndex++; + File out = new File(CACHE_PATH + "/Layer" + layerId + "-" + currentIndex + ".png"); + ImageIO.write(image, "png", out); + } catch (IOException e) { + logger.warning("Exception while caching :" + e.getMessage()); + } + logger.info("Caching successful"); + } + + /** + * Purge redundant images, then do a normal commit. + */ + private void purgeAndCommit(BufferedImage image) { + int numDeleted = 0; + for (int i = currentIndex + 1; i < currentSize; i++) { + File toDelete = new File(CACHE_PATH + "/Layer" + layerId + "-" + i + ".png"); + toDelete.delete(); + numDeleted++; + } + // Reduce the current size depending on the number of images deleted. + currentSize = currentSize - numDeleted; + + normalCommit(image); + } + + /** + * Get the current image state from cache. + */ + public BufferedImage getImage() { + BufferedImage imageFromCache = null; + try { + File in = new File(CACHE_PATH + "/Layer" + layerId + "-" + currentIndex + ".png"); + imageFromCache = ImageIO.read(in); + } catch (IOException e) { + logger.warning("Error reading from cache."); + } + logger.info("Reading from cache successful."); + return imageFromCache; + } + + /** + * Get the current image path from cache. + */ + public Path getCurrentPath() { + File f = new File(CACHE_PATH + "/Layer" + layerId + "-" + currentIndex + ".png"); + return f.toPath(); + } + + public TransformationSet getTransformationSet() { + return transformationSet; + } + + /** + * Adds a transformation to the image + * @param t + */ + public void addTransformation(Transformation t) { + if (currentIndex != transformationSet.getTransformations().size()) { + LinkedList linkedList = new LinkedList<>( + transformationSet.getTransformations().subList(0, currentIndex)); + transformationSet.setTransformations(linkedList); + } + + transformationSet.addTransformations(t); + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of previewImageState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of previewImageState list, unable to redo."); + } + } + + public ArrayList getTransformationsAsString() { + ArrayList output = new ArrayList<>(); + LinkedList sub = + new LinkedList<> (transformationSet.getTransformations().subList(0, currentIndex)); + for (Transformation t : sub) { + output.add(t.toString()); + } + return output; + } +} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6ddc2cd9a290..000000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 980b2b388852..ca1603076e77 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -1,9 +1,15 @@ package seedu.address.model; +import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Objects; +import javax.activation.MimetypesFileTypeMap; + import seedu.address.commons.core.GuiSettings; /** @@ -11,8 +17,11 @@ */ public class UserPrefs { + private static int currBatchPointer = 0; + private GuiSettings guiSettings; - private Path addressBookFilePath = Paths.get("data" , "addressbook.xml"); + private Path currDirectory = Paths.get(System.getProperty("user.home")); + private ArrayList imageList = new ArrayList<>(); public UserPrefs() { setGuiSettings(500, 500, 0, 0); @@ -30,13 +39,85 @@ public void setGuiSettings(double width, double height, int x, int y) { guiSettings = new GuiSettings(width, height, x, y); } - public Path getAddressBookFilePath() { - return addressBookFilePath; + // @@author benedictcss + public Path getCurrDirectory() { + return currDirectory; + } + + /** + * Update the current directory {@code currDirectory} with the new directory + * {@code newCurrDirectory} + */ + public void updateUserPrefs(Path newCurrDirectory) { + this.currDirectory = newCurrDirectory; + initImageList(); + } + + /** + * Update the list of images {@code imageList} with the images found in current directory + * {@code currDirectory} + */ + public void initImageList() { + File currFileDir = new File(currDirectory.toString()); + File[] currFiles = currFileDir.listFiles(); + ArrayList dirImageList = new ArrayList<>(); + try { + for (File file : currFiles) { + if (file.isFile()) { + String mimetype = new MimetypesFileTypeMap().getContentType(file); + // only list if is image + if ("image".equals(mimetype.split("/")[0]) && !"image/gif".equals(mimetype)) { + dirImageList.add(file.toPath()); + } + } + } + } catch (NullPointerException e) { + e.printStackTrace(); + } + Collections.sort(dirImageList); + imageList = dirImageList; + currBatchPointer = 0; + } + + /** + * Returns the total number of images in {@code imageList} + */ + public int getTotalImagesInDir() { + return imageList.size(); + } + + /** + * Returns the {@code currBatchPointer} + */ + public int numOfRemainingImagesInDir() { + return getTotalImagesInDir() - getCurrBatchPointer(); + } + + /** + * Returns the current batch pointer in {@code UserPrefs} + */ + public int getCurrBatchPointer() { + return currBatchPointer; + } + + /** + * Update the {@code currBatchPointer} to the next 10 images + */ + public void updateImageListNextBatch() { + currBatchPointer += 10; + } + + /** + * Update the {@code currBatchPointer} to the prev 10 images + */ + public void updateImageListPrevBatch() { + currBatchPointer -= 10; } - public void setAddressBookFilePath(Path addressBookFilePath) { - this.addressBookFilePath = addressBookFilePath; + public List getCurrImageListBatch() { + return imageList.subList(currBatchPointer, Math.min(currBatchPointer + 10, getTotalImagesInDir())); } + // @@author @Override public boolean equals(Object other) { @@ -49,20 +130,18 @@ public boolean equals(Object other) { UserPrefs o = (UserPrefs) other; - return Objects.equals(guiSettings, o.guiSettings) - && Objects.equals(addressBookFilePath, o.addressBookFilePath); + return Objects.equals(guiSettings, o.guiSettings); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings.toString()); - sb.append("\nLocal data file location : " + addressBookFilePath); return sb.toString(); } diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedAddressBook.java deleted file mode 100644 index 227a335045d7..000000000000 --- a/src/main/java/seedu/address/model/VersionedAddressBook.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.model; - -import java.util.ArrayList; -import java.util.List; - -/** - * {@code AddressBook} that keeps track of its own history. - */ -public class VersionedAddressBook extends AddressBook { - - private final List addressBookStateList; - private int currentStatePointer; - - public VersionedAddressBook(ReadOnlyAddressBook initialState) { - super(initialState); - - addressBookStateList = new ArrayList<>(); - addressBookStateList.add(new AddressBook(initialState)); - currentStatePointer = 0; - } - - /** - * Saves a copy of the current {@code AddressBook} state at the end of the state list. - * Undone states are removed from the state list. - */ - public void commit() { - removeStatesAfterCurrentPointer(); - addressBookStateList.add(new AddressBook(this)); - currentStatePointer++; - } - - private void removeStatesAfterCurrentPointer() { - addressBookStateList.subList(currentStatePointer + 1, addressBookStateList.size()).clear(); - } - - /** - * Restores the address book to its previous state. - */ - public void undo() { - if (!canUndo()) { - throw new NoUndoableStateException(); - } - currentStatePointer--; - resetData(addressBookStateList.get(currentStatePointer)); - } - - /** - * Restores the address book to its previously undone state. - */ - public void redo() { - if (!canRedo()) { - throw new NoRedoableStateException(); - } - currentStatePointer++; - resetData(addressBookStateList.get(currentStatePointer)); - } - - /** - * Returns true if {@code undo()} has address book states to undo. - */ - public boolean canUndo() { - return currentStatePointer > 0; - } - - /** - * Returns true if {@code redo()} has address book states to redo. - */ - public boolean canRedo() { - return currentStatePointer < addressBookStateList.size() - 1; - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof VersionedAddressBook)) { - return false; - } - - VersionedAddressBook otherVersionedAddressBook = (VersionedAddressBook) other; - - // state check - return super.equals(otherVersionedAddressBook) - && addressBookStateList.equals(otherVersionedAddressBook.addressBookStateList) - && currentStatePointer == otherVersionedAddressBook.currentStatePointer; - } - - /** - * Thrown when trying to {@code undo()} but can't. - */ - public static class NoUndoableStateException extends RuntimeException { - private NoUndoableStateException() { - super("Current state pointer at start of addressBookState list, unable to undo."); - } - } - - /** - * Thrown when trying to {@code redo()} but can't. - */ - public static class NoRedoableStateException extends RuntimeException { - private NoRedoableStateException() { - super("Current state pointer at end of addressBookState list, unable to redo."); - } - } -} diff --git a/src/main/java/seedu/address/model/canvas/Canvas.java b/src/main/java/seedu/address/model/canvas/Canvas.java new file mode 100644 index 000000000000..a1ecb05fe2b3 --- /dev/null +++ b/src/main/java/seedu/address/model/canvas/Canvas.java @@ -0,0 +1,179 @@ +package seedu.address.model.canvas; + +import java.util.ArrayList; +import java.util.Collections; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.IllegalOperationException; +import seedu.address.model.PreviewImage; + + +//@@author j-lum +/** + * Represents a working canvas. + * Guarantees that there is at least one layer remaining on the canvas + * and that the canvas area must be larger than zero. + */ +public class Canvas { + + public static final String OUTPUT_ERROR_CURRENT_LAYER = "You cannot remove the layer you're currently working on!"; + public static final String OUTPUT_ERROR_ONLY_LAYER = "You cannot remove the only layer in a canvas!"; + + private static final String LAYER_NAME = "Layer %d"; + private String backgroundColor = "none"; + private ArrayList layers = new ArrayList<>(); + private Layer currentLayer; + private Index currentLayerIndex; + private Boolean isCanvasAuto; + private int layerNameCounts; + private int height; + private int width; + + /** + * Constructor for a canvas that has the size of the initial image. + * Auto-resizing of the canvas defaults to false. + * @param initial - The image which the first layer will be created with. The canvas will take the height and width + * of this image. + */ + public Canvas(PreviewImage initial) { + height = initial.getImage().getHeight(); + width = initial.getImage().getWidth(); + addLayer(initial); + layerNameCounts = 1; + currentLayerIndex = Index.fromZeroBased(0); + currentLayer = layers.get(currentLayerIndex.getZeroBased()); + isCanvasAuto = false; + } + + //Layer operations + + public ArrayList getLayers() { + return layers; + } + + /** + * Returns the list of layer names as an {@code ArrayList} for the LayerPanel. + * * @return a {@code ArrayList} of all the layers in the canvas in order. + */ + + public ArrayList getLayerNames() { + ArrayList names = new ArrayList<>(); + int i = 1; + for (Layer l : layers) { + names.add(i + ": " + l.getName()); + i++; + } + return names; + } + + public void addLayer(PreviewImage i, String name) { + layers.add(new Layer(i, name)); + } + + /** + * Adds a layer without a specified name. A non-unique name will be generated. + * @param i - PreviewImage of the new Layer. + */ + public void addLayer(PreviewImage i) { + layerNameCounts++; + layers.add(new Layer(i, String.format(LAYER_NAME, layerNameCounts))); + } + + /** + * Removes a layer from the canvas. If the only layer left is being removed, + * throws an {@code IllegalOperationException}. + * @param i - Index of the layer to remove. + */ + + public void setCurrentLayer(Index i) { + currentLayerIndex = i; + currentLayer = layers.get(currentLayerIndex.getZeroBased()); + } + + /** + * Removes a layer from the canvas. If the only layer left is being remove, throws an IllegalOperationException. + * @param i - Index of the layer to remove + */ + + public Index removeLayer(Index i) throws IllegalOperationException { + if (layers.size() <= 1) { + throw new IllegalOperationException(OUTPUT_ERROR_ONLY_LAYER); + } + if (i.getZeroBased() == currentLayerIndex.getZeroBased()) { + throw new IllegalOperationException(OUTPUT_ERROR_CURRENT_LAYER); + } + layers.remove(i.getZeroBased()); + if (i.getZeroBased() < currentLayerIndex.getZeroBased()) { + currentLayerIndex = Index.fromZeroBased(currentLayerIndex.getZeroBased() - 1); + } + return currentLayerIndex; + } + + + /** + * Function to swap two layers if neither of them are locked. + * Throws an {@code IndexOutOfBounds} if indexes provided are not valid. + * Throws an {@code IllegalOperationException} if any of the layers are locked. + * @param to A zero-based index within bounds + * @param from A zero-based index within bounds + */ + public void swapLayer(Index to, Index from) throws IllegalOperationException { + if (!to.equals(from)) { + if (currentLayerIndex.getZeroBased() == to.getZeroBased()) { + currentLayerIndex = Index.fromZeroBased(from.getZeroBased()); + } else if (currentLayerIndex.getZeroBased() == from.getZeroBased()) { + currentLayerIndex = Index.fromZeroBased(to.getZeroBased()); + } + + Collections.swap(layers, to.getZeroBased(), from.getZeroBased()); + } else { + throw new IllegalOperationException("Invalid indexes provided!"); + } + } + + // Misc accessors + public Layer getCurrentLayer() { + return currentLayer; + } + + public void setCurrentLayerPosition(int x, int y) { + currentLayer.setPosition(x, y); + } + + public Index getCurrentLayerIndex() { + return currentLayerIndex; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public Boolean isCanvasAuto() { + return isCanvasAuto; + } + + public void setCanvasAuto(Boolean isCanvasAuto) { + this.isCanvasAuto = isCanvasAuto; + } + + public String getBackgroundColor() { + return backgroundColor; + } + + public void setBackgroundColor(String color) { + backgroundColor = color; + } + +} diff --git a/src/main/java/seedu/address/model/canvas/Layer.java b/src/main/java/seedu/address/model/canvas/Layer.java new file mode 100644 index 000000000000..3264621105af --- /dev/null +++ b/src/main/java/seedu/address/model/canvas/Layer.java @@ -0,0 +1,96 @@ +package seedu.address.model.canvas; + +//@@author j-lum +import static java.util.Objects.requireNonNull; + +import seedu.address.model.PreviewImage; +import seedu.address.model.transformation.Transformation; + +/** + * Represents a layer in a canvas. + * Guarantees that the image in the layer is not null. + */ + +public class Layer { + + private final PreviewImage image; + private int x; + private int y; + private int height; + private int width; + private String name; + + //Constructs a new Layer that has a default position (0, 0). + public Layer(PreviewImage image, String name) { + this.image = requireNonNull(image); + this.x = 0; + this.y = 0; + this.height = image.getHeight(); + this.width = image.getWidth(); + this.name = name; + } + + /** + * Adds a transformation to the layer. + * @param t - the transformation to add. + */ + public void addTransformation(Transformation t) { + image.addTransformation(t); + } + + /** + * Sets the position of the layer to the provided co-ordinates + * @param x - the new X position of the layer + * @param y - the new Y position of the layer + */ + public void setPosition(int x, int y) { + setX(x); + setY(y); + } + + //Misc accessors. + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } + + public int getY() { + return y; + } + + public void setY(int y) { + this.y = y; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public PreviewImage getImage() { + return image; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/src/main/java/seedu/address/model/google/PhotoHandler.java b/src/main/java/seedu/address/model/google/PhotoHandler.java new file mode 100644 index 000000000000..960ae48b919e --- /dev/null +++ b/src/main/java/seedu/address/model/google/PhotoHandler.java @@ -0,0 +1,445 @@ +package seedu.address.model.google; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_ALBUM_REQUESTED; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_IMAGE_REQUESTED; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.imageio.ImageIO; + +import com.google.photos.library.v1.PhotosLibraryClient; +import com.google.photos.library.v1.internal.InternalPhotosLibraryClient.ListAlbumsPagedResponse; +import com.google.photos.library.v1.internal.InternalPhotosLibraryClient.ListMediaItemsPagedResponse; +import com.google.photos.library.v1.internal.InternalPhotosLibraryClient.SearchMediaItemsPagedResponse; +import com.google.photos.library.v1.proto.Album; +import com.google.photos.library.v1.proto.BatchCreateMediaItemsRequest; +import com.google.photos.library.v1.proto.BatchCreateMediaItemsResponse; +import com.google.photos.library.v1.proto.MediaItem; +import com.google.photos.library.v1.proto.NewMediaItem; +import com.google.photos.library.v1.proto.NewMediaItemResult; +import com.google.photos.library.v1.upload.UploadMediaItemRequest; +import com.google.photos.library.v1.upload.UploadMediaItemResponse; +import com.google.photos.library.v1.util.NewMediaItemFactory; +import com.google.rpc.Code; +import com.google.rpc.Status; + +import seedu.address.logic.commands.exceptions.CommandException; + +//@@author chivent + +/** + * A middle-man giving access to Google Photos, translates commands to call appropriate methods. + */ +public class PhotoHandler { + + public static final String PICONSO_ALBUM = "Piconso Uploads"; + public static final String WRONG_PATH = "%s does not exist in folder!"; + public static final String UPLOAD_FORMAT = "\n%s >> saved as >> %s"; + + private PhotosLibraryClient photosLibraryClient; + private String user; + private Map albumMap = new HashMap<>(); + private Map imageMap = new HashMap<>(); + private Map albumSpecificMap = new HashMap<>(); + + public PhotoHandler(PhotosLibraryClient client, String email) { + photosLibraryClient = client; + user = email; + } + + public PhotoHandler(String email, Map albumMap, Map imageMap) { + photosLibraryClient = null; + user = email; + this.albumMap = albumMap; + this.imageMap = imageMap; + } + + //=========== Listing Images (ls command) ================================ + + /** + * Returns a list of image names and saves a copy in albumSpecificMap for later reference + * + * @return list of image names + */ + public List returnAllImagesList() { + if (imageMap.isEmpty()) { + retrieveAllImagesFromGoogle(); + } + return new ArrayList<>(imageMap.keySet()); + } + + /** + * Returns a list of album names and saves a copy in albumSpecificMap for later reference + * + * @return list of album names + */ + public List returnAllAlbumsList() { + + if (albumMap.isEmpty()) { + retrieveAllAlbumsFromGoogle(); + } + return new ArrayList<>(albumMap.keySet()); + } + + /** + * Returns list of images in a specified album and saves a copy in albumSpecificMap for later reference + * + * @param albumName name of album to look in + * @return list of image names + */ + public List returnAllImagesinAlbum(String albumName) throws CommandException { + + retrieveSpecificAlbumGoogle(albumName); + + return new ArrayList<>(albumSpecificMap.keySet()); + } + + /** + * Refreshes stored lists of album and image names. + */ + public void refreshLists() { + retrieveAllAlbumsFromGoogle(); + retrieveAllImagesFromGoogle(); + } + + + //=========== Retrieving Images from Google ================================ + + /** + * Retrieves all albums in Google Photos + */ + private void retrieveAllAlbumsFromGoogle() { + albumMap.clear(); + ListAlbumsPagedResponse albums = photosLibraryClient.listAlbums(); + for (Album album : albums.iterateAll()) { + albumMap.put(getUniqueName(albumMap, album.getTitle(), null), album); + } + } + + /** + * Retrieves all images in Google Photos + */ + private void retrieveAllImagesFromGoogle() { + imageMap.clear(); + ListMediaItemsPagedResponse listMediaItems = photosLibraryClient.listMediaItems(); + for (MediaItem item : listMediaItems.iterateAll()) { + //only store the item if it is an image + if (item.getMimeType().contains("image")) { + String mimeType = "." + item.getMimeType().replace("image/", ""); + imageMap.put(getUniqueName(imageMap, item.getFilename(), mimeType), item); + } + } + } + + /** + * Retrieves all images in specified album + * + * @param albumName name of album to retrieve + */ + private void retrieveSpecificAlbumGoogle(String albumName) throws CommandException { + Album album; + + // if album name not found, re-retrieve all albums (in case un-updated) + if ((album = albumMap.get(albumName)) == null) { + retrieveAllAlbumsFromGoogle(); + if ((album = albumMap.get(albumName)) == null) { + throw new CommandException(MESSAGE_INVALID_ALBUM_REQUESTED); + } + } + + albumSpecificMap.clear(); + + SearchMediaItemsPagedResponse listMediaItems = photosLibraryClient.searchMediaItems(album.getId()); + for (MediaItem item : listMediaItems.iterateAll()) { + //only store the item if it is an image + if (item.getMimeType().contains("image")) { + String mimeType = "." + item.getMimeType().replace("image/", ""); + albumSpecificMap.put(getUniqueName(albumSpecificMap, item.getFilename(), mimeType), item); + } + } + } + + //=========== Downloading Images from Google ================================ + + /** + * Downloads an image from Google Photos + * + * @param imageName name of image to look for + * @param currDir directory to save image in. + * @throws IOException thrown if input/output is invalid + */ + public void downloadImage(String imageName, String currDir) throws CommandException, IOException { + + MediaItem image; + + // if image name not found, re-retrieve album (in case un-updated) + if ((image = imageMap.get(imageName)) == null) { + retrieveAllImagesFromGoogle(); + if ((image = imageMap.get(imageName)) == null) { + throw new CommandException(MESSAGE_INVALID_IMAGE_REQUESTED); + } + } + + saveImageInDir(image, currDir + "/" + imageName); + } + + /** + * Downloads an image from last opened album from Google Photos + * + * @param albumName album to look in + * @param imageName image to look for + * @param currDir directory to save image in. + * @throws IOException thrown if input/output is invalid + */ + public void downloadAlbumImage(String albumName, String imageName, String currDir) + throws CommandException, IOException { + + MediaItem image; + retrieveSpecificAlbumGoogle(albumName); + + // if image name not found, re-retrieve album (in case un-updated) + if ((image = albumSpecificMap.get(imageName)) == null) { + throw new CommandException(MESSAGE_INVALID_IMAGE_REQUESTED); + } + + saveImageInDir(image, currDir + "/" + imageName); + } + + /** + * Downloads all images from an album in Google Photos + * + * @param albumName album to download + * @param currDir directory to save image in. + * @throws IOException thrown if input/output is invalid + */ + public void downloadWholeAlbum(String albumName, String currDir) throws IOException, CommandException { + + retrieveSpecificAlbumGoogle(albumName); + + for (Map.Entry entry : albumSpecificMap.entrySet()) { + //only store the item if it is an image + if (entry.getValue().getMimeType().contains("image")) { + saveImageInDir(entry.getValue(), currDir + "/" + entry.getKey()); + } + } + } + + /** + * Saves the specified image into the current opened directory + * + * @param image MediaItem retrieved from Google Photos + * @param pathName directory to save image in. + * @throws IOException thrown if input/output is invalid + */ + private void saveImageInDir(MediaItem image, String pathName) throws IOException { + String extensionType = image.getMimeType().split("/")[1]; + BufferedImage newImage = ImageIO.read(new URL(image.getBaseUrl() + "=d")); + ImageIO.write(newImage, extensionType, new File(pathName)); + } + + //=========== Uploading Images from Google ================================ + + /** + * Uploads image to Google Photos + * + * @param imageName name of image to be retrieved + * @param pathName directory to upload from + * @return name of image if it is a duplicate. + */ + public String uploadImage(String imageName, String pathName) throws Exception { + if (!Files.exists(Paths.get(pathName + "/" + imageName))) { + throw new Exception(String.format(WRONG_PATH, imageName)); + } + + Map uploads = uploadMediaItemsToGoogle( + Collections.singletonList(generateNewMediaImage(imageName, pathName))); + return formatUploadFeedback(uploads, Collections.singletonList(imageName)); + } + + /** + * Uploads all images in specified directory to Google Photos + * + * @param path directory to upload from + * @return names of non-duplicate images + */ + public String uploadAll(String path) throws Exception { + List newItems = new ArrayList<>(); + List imageNames = new ArrayList<>(); + + File dir = new File(path); + + for (File file : Objects.requireNonNull(dir.listFiles())) { + if (file.isFile() && ImageIO.read(file) != null) { + imageNames.add(file.getName()); + newItems.add(generateNewMediaImage(file.getName(), path)); + } + } + + Map uploads = uploadMediaItemsToGoogle(newItems); + return formatUploadFeedback(uploads, imageNames); + } + + /** + * Generates an upload token from an image name and file path + * + * @param imageName name of image to be retrieved + * @param pathName directory to upload from + * @return NewMediaItem generated from upload token + */ + private NewMediaItem generateNewMediaImage(String imageName, String pathName) throws Exception { + String uploadToken; + + //Get upload token + UploadMediaItemRequest uploadRequest = UploadMediaItemRequest.newBuilder().setFileName(imageName) + .setDataFile(new RandomAccessFile(pathName + "/" + imageName, "r")).build(); + // Upload and capture the response + UploadMediaItemResponse uploadResponse = photosLibraryClient.uploadMediaItem(uploadRequest); + if (uploadResponse.getError().isPresent()) { + UploadMediaItemResponse.Error error = uploadResponse.getError().get(); + throw new Exception(error.getCause()); + } else { + uploadToken = uploadResponse.getUploadToken().get(); + if (!uploadToken.isEmpty()) { + return NewMediaItemFactory.createNewMediaItem(uploadToken); + } else { + throw new Exception("Unable to upload images."); + } + } + } + + /** + * Uploads created NewMediaItems to Google Photos + * + * @param newItems list of NewMediaItems to pass to Google Photos + * @return array of nonDuplicate names + * @throws Exception when uploading error occurs + */ + private Map uploadMediaItemsToGoogle(List newItems) throws Exception { + Map nonDuplicates = new HashMap<>(); + BatchCreateMediaItemsResponse response; + String albumId = retrievePiconsoAlbum(); + + // if album could not be retrieved, just upload photo + if (albumId.isEmpty()) { + response = photosLibraryClient.batchCreateMediaItemsCallable().call( + BatchCreateMediaItemsRequest.newBuilder().addAllNewMediaItems(newItems).build()); + } else { + response = photosLibraryClient.batchCreateMediaItems(albumId, newItems); + } + + int i = 0; + for (NewMediaItemResult itemsResponse : response.getNewMediaItemResultsList()) { + Status status = itemsResponse.getStatus(); + if (status.getCode() != Code.OK_VALUE) { + if (status.getCode() != 6) { + throw new Exception("An error occurred when uploading to Google Photos, please try again"); + } + } else { + nonDuplicates.put(i, itemsResponse.getMediaItem().getFilename()); + } + i++; + } + + return nonDuplicates; + } + + //=========== Misc ================================ + + /** + * Returns a string for user email + * + * @return logged in user's email + */ + public String identifyUser() { + return user; + } + + /** + * As Album/Image names can be duplicates in Google Photos, new names to display in CLI are appended with a + * suitable number to differentiate albums/images + * + * @param map Map to be comparing to + * @param title Key to search for + * @param mimeType Extension of image + * @return new title to act as key in map + */ + public String getUniqueName(Map map, String title, String mimeType) { + String newTitle = title; + String titleWithoutExtension; + String extension; + + if (mimeType != null) { + titleWithoutExtension = newTitle.replace(mimeType, ""); + extension = mimeType; + } else { + titleWithoutExtension = title; + extension = ""; + } + int i = 1; + while (map.get(newTitle) != null) { + newTitle = titleWithoutExtension + " (" + i + ")" + extension; + i++; + } + + return newTitle; + } + + /** + * Format upload feedback message + * + * @param uploads map of non-duplicate uploads + * @param imageNames list of images that were to be uploaded + * @return formatted feedback + */ + public String formatUploadFeedback(Map uploads, List imageNames) { + StringBuilder sb = new StringBuilder(); + if (uploads.isEmpty()) { + return ""; + } else { + if (uploads.size() == imageNames.size()) { + sb.append(".all"); + } + + for (Integer entry : uploads.keySet()) { + sb.append(String.format(UPLOAD_FORMAT, imageNames.get(entry), uploads.get(entry))); + } + return sb.toString(); + } + } + + /** + * Creates/Retrieves an album in user's library to store Piconso edited photos. + * + * @return Id of Piconso Album + */ + public String retrievePiconsoAlbum() { + String id; + Album album; + + try { + // if album name not found, re-retrieve all albums (in case un-updated) + if ((album = albumMap.get(PICONSO_ALBUM)) == null) { + retrieveAllAlbumsFromGoogle(); + if ((album = albumMap.get(PICONSO_ALBUM)) == null) { + album = photosLibraryClient.createAlbum(Album.newBuilder().setTitle(PICONSO_ALBUM).build()); + retrieveAllAlbumsFromGoogle(); + } + } + id = album.getId(); + } catch (Exception ex) { + id = ""; + } + return id; + } +} diff --git a/src/main/java/seedu/address/model/google/PhotosLibraryClientFactory.java b/src/main/java/seedu/address/model/google/PhotosLibraryClientFactory.java new file mode 100644 index 000000000000..8ad28a3da205 --- /dev/null +++ b/src/main/java/seedu/address/model/google/PhotosLibraryClientFactory.java @@ -0,0 +1,184 @@ +package seedu.address.model.google; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.util.List; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.auth.oauth2.StoredCredential; +import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; +import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.client.util.store.DataStoreFactory; +import com.google.api.client.util.store.FileDataStoreFactory; +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.services.plus.Plus; +import com.google.api.services.plus.model.Person; +import com.google.auth.Credentials; +import com.google.auth.oauth2.UserCredentials; +import com.google.common.collect.ImmutableList; +import com.google.photos.library.v1.PhotosLibraryClient; +import com.google.photos.library.v1.PhotosLibrarySettings; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.events.ui.LoginStatusEvent; +import seedu.address.commons.util.FileUtil; + +//@@author chivent + +/** + * Factory to create instance of PhotosLibraryClient + */ +public class PhotosLibraryClientFactory { + + public static final File DATA_STORE = Paths.get("./src/main/resources/user_credentials").toFile(); + + //to be created on testing. prevents tests from launching login process + public static final File TEST_FILE = Paths.get("./src/main/resources/user_credentials/TEST_FILE.txt").toFile(); + + //to be created on login process start and deleted only on login process end. prevents unnecessary redirects + public static final File BLOCKER = Paths.get("./src/main/resources/user_credentials/BLOCKER.txt").toFile(); + + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + private static final List SCOPE_LIST = + ImmutableList.of( + "https://www.googleapis.com/auth/photoslibrary", + "https://www.googleapis.com/auth/photoslibrary.sharing", + "email"); + + private static final String CREDENTIAL_FILE = "client_credentials.json"; + + private PhotosLibraryClientFactory() { + } + + /** + * Creates and returns a new PhotoHandler + * + * @return PhotoHandler + * @throws IOException when files cannot be read + * @throws GeneralSecurityException when there is an error with authentication + */ + public static PhotoHandler createClient() throws IOException, GeneralSecurityException { + FileUtil.createDirectoriesIfMissing(DATA_STORE); + + //@@author chivent-reused + //Reused from https://github.com/google/java-photoslibrary/blob/master/sample/src/main/ + //java/com/google/photos/library/sample/demos/AlbumDemo.java with minor modifications + DataStoreFactory dataStoreFactory = new FileDataStoreFactory(DATA_STORE); + + InputStream credentialFile = PhotosLibraryClientFactory + .class.getClassLoader().getResourceAsStream(CREDENTIAL_FILE); + + // load designated client secret/id + GoogleClientSecrets clientSecrets = + GoogleClientSecrets.load( + JSON_FACTORY, new InputStreamReader(credentialFile)); + + String clientSecret = clientSecrets.getDetails().getClientSecret(); + String clientId = clientSecrets.getDetails().getClientId(); + + // TODO: v2.0 develop a more intuitive way of avoiding this issue + // Currently BLOCKER.TXT is created because Google automatically creates a blank credential file when login, + // is launched even if user does not actually login. This causes our app to always redirect to google login + // whenever a blank credential file is found on start up. + try { + FileUtil.createIfMissing(BLOCKER); + } catch (Exception e) { + e.printStackTrace(); + } + + if (!TEST_FILE.exists()) { + //google standard authorization flow + GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( + GoogleNetHttpTransport.newTrustedTransport(), JSON_FACTORY, + clientSecrets, SCOPE_LIST).setDataStoreFactory(dataStoreFactory).build(); + + // Credential is a google construct that wraps the access token and helps you to refresh periodically + // AuthorizationCodeInstalledApp is another google standard that helps persist user end credentials + Credential credential = new AuthorizationCodeInstalledApp(flow, + new LocalServerReceiver()).authorize("user"); + + //UserCredentials is a specific credential type that stores user specific credentials + UserCredentials userCredentials = UserCredentials.newBuilder().setClientId(clientId) + .setClientSecret(clientSecret).setRefreshToken(credential.getRefreshToken()).build(); + + credentialFile.close(); + FileUtil.deleteIfAvaliable(BLOCKER); + return new PhotoHandler(createPhotosLibraryClient(userCredentials), getUserEmail(credential)); + } else { + FileUtil.deleteIfAvaliable(BLOCKER); + return null; + } + } + + /** + * Creates a PhotosLibraryClient instance from credentials + * + * @param credentials + * @return PhotosLibraryClient new PhotosLibraryClient instance + */ + private static PhotosLibraryClient createPhotosLibraryClient(Credentials credentials) throws IOException { + PhotosLibrarySettings libSettings = PhotosLibrarySettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)).build(); + return PhotosLibraryClient.initialize(libSettings); + } + + /** + * Gets user's email from a Google+ instance + * + * @param credential credentials to create google+ instance with + * @return a String of user email + */ + private static String getUserEmail(Credential credential) throws GeneralSecurityException, IOException { + Plus plus = new Plus.Builder(GoogleNetHttpTransport.newTrustedTransport(), + JacksonFactory.getDefaultInstance(), credential).setApplicationName("Piconso").build(); + + List emails = plus.people().get("me").execute().getEmails(); + + EventsCenter.getInstance().post(new LoginStatusEvent(emails.get(0).getValue())); + return emails.get(0).getValue(); + } + + /** + * Checks if a user has storedCredentials (did not logout previously), and auto log ins user if true. + * + * @return a PhotoHandler instance if user has storedCredentials, else null + */ + public static PhotoHandler loginUserIfPossible() throws IOException, GeneralSecurityException { + return checkUserLogin() ? createClient() : null; + } + + /** + * Logs user out of currently logged in account + */ + public static boolean logoutUserIfPossible() { + boolean userLoggedOut; + if (userLoggedOut = checkUserLogin()) { + File[] listFiles = DATA_STORE.listFiles(); + for (File file : listFiles) { + file.delete(); + + } + } + return userLoggedOut; + } + + /** + * Checks if a user has storedCredentials (did not logout previously). + * + * @return true if user has storedCredentials, else null + */ + public static boolean checkUserLogin() { + return new File(DATA_STORE, StoredCredential.DEFAULT_DATA_STORE_ID).exists(); + } +} + + diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index a1409233ceb9..000000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,58 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_ADDRESS_CONSTRAINTS = - "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String ADDRESS_VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_ADDRESS_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(ADDRESS_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index 38a7629e9a2d..000000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - private static final String SPECIAL_CHARACTERS = "!#$%&'*+/=?`{|}~^.-"; - public static final String MESSAGE_EMAIL_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + ") .\n" - + "2. This is followed by a '@' and then a domain name. " - + "The domain name must:\n" - + " - be at least 2 characters long\n" - + " - start and end with alphanumeric characters\n" - + " - consist of alphanumeric characters, a period or a hyphen for the characters in between, if any."; - // alphanumeric and special characters - private static final String LOCAL_PART_REGEX = "^[\\w" + SPECIAL_CHARACTERS + "]+"; - private static final String DOMAIN_FIRST_CHARACTER_REGEX = "[^\\W_]"; // alphanumeric characters except underscore - private static final String DOMAIN_MIDDLE_REGEX = "[a-zA-Z0-9.-]*"; // alphanumeric, period and hyphen - private static final String DOMAIN_LAST_CHARACTER_REGEX = "[^\\W_]$"; - public static final String EMAIL_VALIDATION_REGEX = LOCAL_PART_REGEX + "@" - + DOMAIN_FIRST_CHARACTER_REGEX + DOMAIN_MIDDLE_REGEX + DOMAIN_LAST_CHARACTER_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_EMAIL_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(EMAIL_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java deleted file mode 100644 index 9982393dabb5..000000000000 --- a/src/main/java/seedu/address/model/person/Name.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's name in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} - */ -public class Name { - - public static final String MESSAGE_NAME_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; - - public final String fullName; - - /** - * Constructs a {@code Name}. - * - * @param name A valid name. - */ - public Name(String name) { - requireNonNull(name); - checkArgument(isValidName(name), MESSAGE_NAME_CONSTRAINTS); - fullName = name; - } - - /** - * Returns true if a given string is a valid name. - */ - public static boolean isValidName(String test) { - return test.matches(NAME_VALIDATION_REGEX); - } - - - @Override - public String toString() { - return fullName; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check - } - - @Override - public int hashCode() { - return fullName.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427ca..000000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 557a7a60cd51..000000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.model.tag.Tag; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. - */ -public class Person { - - // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - - /** - * Every field must be present and not null. - */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - public Phone getPhone() { - return phone; - } - - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - - /** - * Returns true if both persons of the same name have at least one other identity field that is the same. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append(" Phone: ") - .append(getPhone()) - .append(" Email: ") - .append(getEmail()) - .append(" Address: ") - .append(getAddress()) - .append(" Tags: "); - getTags().forEach(builder::append); - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index a22e51653835..000000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - - public static final String MESSAGE_PHONE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String PHONE_VALIDATION_REGEX = "\\d{3,}"; - public final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_PHONE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(PHONE_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 5856aa42e6b5..000000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,135 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return FXCollections.unmodifiableObservableList(internalList); - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f594423..000000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca73..000000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index 8cdff2773ac9..000000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_TAG_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String TAG_VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_TAG_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(TAG_VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && tagName.equals(((Tag) other).tagName)); // state check - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/transformation/Transformation.java b/src/main/java/seedu/address/model/transformation/Transformation.java new file mode 100644 index 000000000000..e9218c1987c4 --- /dev/null +++ b/src/main/java/seedu/address/model/transformation/Transformation.java @@ -0,0 +1,57 @@ +package seedu.address.model.transformation; + +//@@uthor j-lum + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Represents a single transformation to a single layer in a canvas. + */ +public class Transformation { + + private String operation; + private String[] args; + + public Transformation(String operation, String... args) { + this.operation = operation; + this.args = args; + } + + //@@author lancelotwillow + @Override + public String toString() { + String result = operation; + for (int i = 0; i < args.length; i++) { + result += " " + args[i]; + } + return result; //whoever is in charge do this + } + + //@@author lancelotwillow + /** + * return a list of String that contains all the arguments and the operation + * @return + */ + public ArrayList toList() { + ArrayList list = new ArrayList<>(); + list.add(operation); + list.addAll(Arrays.asList(args)); + return list; + } + + public String getOperation() { + return operation; + } + + @Override + public boolean equals(Object object) { + Transformation transformation = (Transformation) object; + if (transformation == this) { + return true; + } else { + return operation.equals(transformation.operation) + && Arrays.equals(args, transformation.args); + } + } +} diff --git a/src/main/java/seedu/address/model/transformation/TransformationSet.java b/src/main/java/seedu/address/model/transformation/TransformationSet.java new file mode 100644 index 000000000000..3be79d5d34e9 --- /dev/null +++ b/src/main/java/seedu/address/model/transformation/TransformationSet.java @@ -0,0 +1,37 @@ +package seedu.address.model.transformation; + +import java.util.LinkedList; + +//@@author j-lum + +/** + * Represents a set of transformations that can be applied to any layer. + */ +public class TransformationSet { + private LinkedList transformations; + //private Index cursor; + + /** + * Default constructor. + */ + public TransformationSet() { + transformations = new LinkedList<>(); + //cursor = Index.fromZeroBased(0); + } + + /** + * @param t + * Adds transformations to transformation set + */ + public void addTransformations(Transformation t) { + transformations.addLast(t); + //cursor = Index.fromOneBased(transformations.size()); + } + + public LinkedList getTransformations() { + return transformations; + } + public void setTransformations(LinkedList list) { + transformations = list; + } +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facfa..000000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f92..000000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/JsonConvertArgsStorage.java b/src/main/java/seedu/address/storage/JsonConvertArgsStorage.java new file mode 100644 index 000000000000..4f362f66a23f --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonConvertArgsStorage.java @@ -0,0 +1,103 @@ +package seedu.address.storage; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import seedu.address.commons.util.ResourceUtil; +import seedu.address.model.transformation.Transformation; + + +/** + * @@author lancelotwillow + * this class is to convert the joson file that store the convert arguments to list[String], and store the convert + */ +public class JsonConvertArgsStorage { + + /** + * . + * @param name + * @param cmds + * @throws IOException + */ + public static void storeArgument(String name, List cmds, String saveFolder) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode objectNode1 = mapper.createObjectNode(); + //create node for the json file + objectNode1.put("name", name); + objectNode1.put("num", cmds.size()); + ObjectNode operation = mapper.createObjectNode(); + for (int i = 1; i <= cmds.size(); i++) { + ObjectNode currentOperation = mapper.createObjectNode(); + currentOperation.put("name", cmds.get(i - 1).toList().get(0)); + int argNum = cmds.get(i - 1).toList().size() - 1; + currentOperation.put("num", argNum); + ObjectNode argument = mapper.createObjectNode(); + for (int j = 1; j <= argNum; j++) { + argument.put("arg" + j, cmds.get(i - 1).toList().get(j)); + } + currentOperation.putPOJO("args", argument); + operation.putPOJO("op" + i, currentOperation); + } + objectNode1.putPOJO("operations", operation); + byte[] content = mapper.writer().writeValueAsString(objectNode1).getBytes(); + File command = new File(saveFolder + "/" + name + ".json"); + //write the json content to the file + BufferedOutputStream bio = new BufferedOutputStream(new FileOutputStream(command)); + bio.write(content); + bio.write("\n".getBytes()); + bio.flush(); + } + + /** + * get the template of the arguments need for the operation + * @param fileUrl + * @param operation + * @return + * @throws IOException + */ + public static List retrieveCommandTemplate(URL fileUrl, String operation, String content) + throws IOException { + if (fileUrl == null || operation == null) { + throw new IOException("the url is invalid"); + } + File file = new File("commandTemplate.json"); + ResourceUtil.copyResourceFileOut(fileUrl, file); + JsonNode jsonNode = new ObjectMapper().readTree(file); + List patterns = new ArrayList<>(); + int num = jsonNode.get("num").asInt(); + for (int i = 1; i <= num; i++) { + String pattern = jsonNode.get(content + "s").get(content + i).textValue(); + patterns.add(pattern); + } + file.delete(); + return patterns; + } + + /** + * . + * @param file + * @return + */ + public static List retrieveCommandArguments(File file) throws IOException { + JsonNode jsonNode = new ObjectMapper().readTree(file); + List args = new ArrayList<>(); + int num = jsonNode.get("num").asInt(); + for (int i = 1; i <= num; i++) { + JsonNode currentOp = jsonNode.get("operations").get("op" + i); + args.add((i == 1 ? "" : "-") + currentOp.get("name").textValue()); + for (int j = 1; j <= currentOp.get("num").asInt(); j++) { + args.add(currentOp.get("args").get("arg" + j).textValue()); + } + } + return args; + } +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 28791127999b..3c27c6368f78 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -1,19 +1,15 @@ package seedu.address.storage; import java.io.IOException; -import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends UserPrefsStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -21,19 +17,5 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override void saveUserPrefs(UserPrefs userPrefs) throws IOException; - @Override - Path getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataConversionException, IOException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * Saves the current version of the Address Book to the hard disk. - * Creates the data file if it is missing. - * Raises {@link DataSavingExceptionEvent} if there was an error during saving. - */ - void handleAddressBookChangedEvent(AddressBookChangedEvent abce); + void clearCache(); } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index b0df908a76a7..60aae702194b 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -1,33 +1,27 @@ package seedu.address.storage; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import java.util.logging.Logger; -import com.google.common.eventbus.Subscribe; - import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.model.AddressBookChangedEvent; -import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; /** - * Manages storage of AddressBook data in local storage. + * Manages storage of Piconso data in local storage. */ public class StorageManager extends ComponentManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(UserPrefsStorage userPrefsStorage) { super(); - this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; } @@ -48,46 +42,15 @@ public void saveUserPrefs(UserPrefs userPrefs) throws IOException { userPrefsStorage.saveUserPrefs(userPrefs); } - - // ================ AddressBook methods ============================== - - @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); - } - - @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); - } - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); - } - - - @Override - @Subscribe - public void handleAddressBookChangedEvent(AddressBookChangedEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); - try { - saveAddressBook(event.data); - } catch (IOException e) { - raise(new DataSavingExceptionEvent(e)); + public void clearCache() { + File cache = new File("cache"); + File[] list = cache.listFiles(); + if (list != null) { + for (File file: list) { + file.delete(); + } } + logger.info("Cache cleared."); } - } diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/address/storage/UserPrefsStorage.java index 877b0ee5c4f0..f49b2914534a 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/UserPrefsStorage.java @@ -8,7 +8,7 @@ import seedu.address.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link UserPrefs}. */ public interface UserPrefsStorage { @@ -26,7 +26,7 @@ public interface UserPrefsStorage { Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.UserPrefs} to the storage. + * Saves the given {@link UserPrefs} to the storage. * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java deleted file mode 100644 index c03785e5700f..000000000000 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.xml.bind.annotation.XmlElement; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * JAXB-friendly version of the Person. - */ -public class XmlAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - @XmlElement(required = true) - private String name; - @XmlElement(required = true) - private String phone; - @XmlElement(required = true) - private String email; - @XmlElement(required = true) - private String address; - - @XmlElement - private List tagged = new ArrayList<>(); - - /** - * Constructs an XmlAdaptedPerson. - * This is the no-arg constructor that is required by JAXB. - */ - public XmlAdaptedPerson() {} - - /** - * Constructs an {@code XmlAdaptedPerson} with the given person details. - */ - public XmlAdaptedPerson(String name, String phone, String email, String address, List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged = new ArrayList<>(tagged); - } - } - - /** - * Converts a given Person into this class for JAXB use. - * - * @param source future changes to this will not affect the created XmlAdaptedPerson - */ - public XmlAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged = source.getTags().stream() - .map(XmlAdaptedTag::new) - .collect(Collectors.toList()); - } - - /** - * Converts this jaxb-friendly adapted person object into the model's Person object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (XmlAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_NAME_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_PHONE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_EMAIL_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_ADDRESS_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof XmlAdaptedPerson)) { - return false; - } - - XmlAdaptedPerson otherPerson = (XmlAdaptedPerson) other; - return Objects.equals(name, otherPerson.name) - && Objects.equals(phone, otherPerson.phone) - && Objects.equals(email, otherPerson.email) - && Objects.equals(address, otherPerson.address) - && tagged.equals(otherPerson.tagged); - } -} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedTag.java b/src/main/java/seedu/address/storage/XmlAdaptedTag.java deleted file mode 100644 index d3e2d8be9c4f..000000000000 --- a/src/main/java/seedu/address/storage/XmlAdaptedTag.java +++ /dev/null @@ -1,62 +0,0 @@ -package seedu.address.storage; - -import javax.xml.bind.annotation.XmlValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * JAXB-friendly adapted version of the Tag. - */ -public class XmlAdaptedTag { - - @XmlValue - private String tagName; - - /** - * Constructs an XmlAdaptedTag. - * This is the no-arg constructor that is required by JAXB. - */ - public XmlAdaptedTag() {} - - /** - * Constructs a {@code XmlAdaptedTag} with the given {@code tagName}. - */ - public XmlAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given Tag into this class for JAXB use. - * - * @param source future changes to this will not affect the created - */ - public XmlAdaptedTag(Tag source) { - tagName = source.tagName; - } - - /** - * Converts this jaxb-friendly adapted tag object into the model's Tag object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_TAG_CONSTRAINTS); - } - return new Tag(tagName); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof XmlAdaptedTag)) { - return false; - } - - return tagName.equals(((XmlAdaptedTag) other).tagName); - } -} diff --git a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java deleted file mode 100644 index ecf0e7ec23a8..000000000000 --- a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.storage; - -import static java.util.Objects.requireNonNull; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A class to access AddressBook data stored as an xml file on the hard disk. - */ -public class XmlAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(XmlAddressBookStorage.class); - - private Path filePath; - - public XmlAddressBookStorage(Path filePath) { - this.filePath = filePath; - } - - public Path getAddressBookFilePath() { - return filePath; - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(filePath); - } - - /** - * Similar to {@link #readAddressBook()} - * @param filePath location of the data. Cannot be null - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(Path filePath) throws DataConversionException, - FileNotFoundException { - requireNonNull(filePath); - - if (!Files.exists(filePath)) { - logger.info("AddressBook file " + filePath + " not found"); - return Optional.empty(); - } - - XmlSerializableAddressBook xmlAddressBook = XmlFileStorage.loadDataFromSaveFile(filePath); - try { - return Optional.of(xmlAddressBook.toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)} - * @param filePath location of the data. Cannot be null - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); - requireNonNull(filePath); - - FileUtil.createIfMissing(filePath); - XmlFileStorage.saveDataToFile(filePath, new XmlSerializableAddressBook(addressBook)); - } - -} diff --git a/src/main/java/seedu/address/storage/XmlFileStorage.java b/src/main/java/seedu/address/storage/XmlFileStorage.java deleted file mode 100644 index d8f65dc036ab..000000000000 --- a/src/main/java/seedu/address/storage/XmlFileStorage.java +++ /dev/null @@ -1,39 +0,0 @@ -package seedu.address.storage; - -import java.io.FileNotFoundException; -import java.nio.file.Path; - -import javax.xml.bind.JAXBException; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.XmlUtil; - -/** - * Stores addressbook data in an XML file - */ -public class XmlFileStorage { - /** - * Saves the given addressbook data to the specified file. - */ - public static void saveDataToFile(Path file, XmlSerializableAddressBook addressBook) - throws FileNotFoundException { - try { - XmlUtil.saveDataToFile(file, addressBook); - } catch (JAXBException e) { - throw new AssertionError("Unexpected exception " + e.getMessage(), e); - } - } - - /** - * Returns address book in the file or an empty address book - */ - public static XmlSerializableAddressBook loadDataFromSaveFile(Path file) throws DataConversionException, - FileNotFoundException { - try { - return XmlUtil.getDataFromFile(file, XmlSerializableAddressBook.class); - } catch (JAXBException e) { - throw new DataConversionException(e); - } - } - -} diff --git a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java deleted file mode 100644 index b85fa4a8f07e..000000000000 --- a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to XML format - */ -@XmlRootElement(name = "addressbook") -public class XmlSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - @XmlElement - private List persons; - - /** - * Creates an empty XmlSerializableAddressBook. - * This empty constructor is required for marshalling. - */ - public XmlSerializableAddressBook() { - persons = new ArrayList<>(); - } - - /** - * Conversion - */ - public XmlSerializableAddressBook(ReadOnlyAddressBook src) { - this(); - persons.addAll(src.getPersonList().stream().map(XmlAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this addressbook into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated or duplicates in the - * {@code XmlAdaptedPerson}. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (XmlAdaptedPerson p : persons) { - Person person = p.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof XmlSerializableAddressBook)) { - return false; - } - return persons.equals(((XmlSerializableAddressBook) other).persons); - } -} diff --git a/src/test/data/XmlUtilTest/empty.xml b/src/main/java/seedu/address/storage/singleCommand.json similarity index 100% rename from src/test/data/XmlUtilTest/empty.xml rename to src/main/java/seedu/address/storage/singleCommand.json diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java deleted file mode 100644 index b43de90a2b9f..000000000000 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ /dev/null @@ -1,72 +0,0 @@ -package seedu.address.ui; - -import java.net.URL; -import java.util.logging.Logger; - -import com.google.common.eventbus.Subscribe; - -import javafx.application.Platform; -import javafx.event.Event; -import javafx.fxml.FXML; -import javafx.scene.layout.Region; -import javafx.scene.web.WebView; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.model.person.Person; - -/** - * The Browser Panel of the App. - */ -public class BrowserPanel extends UiPart { - - public static final String DEFAULT_PAGE = "default.html"; - public static final String SEARCH_PAGE_URL = - "https://se-edu.github.io/addressbook-level4/DummySearchPage.html?name="; - - private static final String FXML = "BrowserPanel.fxml"; - - private final Logger logger = LogsCenter.getLogger(getClass()); - - @FXML - private WebView browser; - - public BrowserPanel() { - super(FXML); - - // To prevent triggering events for typing inside the loaded Web page. - getRoot().setOnKeyPressed(Event::consume); - - loadDefaultPage(); - registerAsAnEventHandler(this); - } - - private void loadPersonPage(Person person) { - loadPage(SEARCH_PAGE_URL + person.getName().fullName); - } - - public void loadPage(String url) { - Platform.runLater(() -> browser.getEngine().load(url)); - } - - /** - * Loads a default HTML file with a background that matches the general theme. - */ - private void loadDefaultPage() { - URL defaultPage = MainApp.class.getResource(FXML_FILE_FOLDER + DEFAULT_PAGE); - loadPage(defaultPage.toExternalForm()); - } - - /** - * Frees resources allocated to the browser. - */ - public void freeResources() { - browser = null; - } - - @Subscribe - private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - loadPersonPage(event.getNewSelection()); - } -} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 3d7aaded5640..4cf74e4d8278 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,5 +1,9 @@ package seedu.address.ui; +import java.io.File; +import java.nio.file.Paths; +import java.util.LinkedList; +import java.util.Queue; import java.util.logging.Logger; import javafx.collections.ObservableList; @@ -11,9 +15,11 @@ import seedu.address.commons.events.ui.NewResultAvailableEvent; import seedu.address.logic.ListElementPointer; import seedu.address.logic.Logic; +import seedu.address.logic.commands.CdCommand; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.UserPrefs; /** * The UI component that is responsible for receiving user command inputs. @@ -21,18 +27,26 @@ public class CommandBox extends UiPart { public static final String ERROR_STYLE_CLASS = "error"; + /** + * Used for initial separation of command word and args. + */ + //private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); private static final String FXML = "CommandBox.fxml"; private final Logger logger = LogsCenter.getLogger(CommandBox.class); private final Logic logic; + private final UserPrefs prefs; + private String startDir; + private Queue dirListWithSearchPrefix = new LinkedList<>(); private ListElementPointer historySnapshot; @FXML private TextField commandTextField; - public CommandBox(Logic logic) { + public CommandBox(Logic logic, UserPrefs prefs) { super(FXML); this.logic = logic; + this.prefs = prefs; // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); historySnapshot = logic.getHistorySnapshot(); @@ -55,6 +69,12 @@ private void handleKeyPress(KeyEvent keyEvent) { keyEvent.consume(); navigateToNextInput(); break; + case TAB: + keyEvent.consume(); + completeDirName(); + commandTextField.requestFocus(); + commandTextField.positionCaret(commandTextField.getLength()); + break; default: // let JavaFx handle the keypress } @@ -95,6 +115,106 @@ private void replaceText(String text) { commandTextField.positionCaret(commandTextField.getText().length()); } + //@@author benedictcss + /** + * Auto complete {@code CommandBox}'s text field with the directory name + * if it exists and resets {@code startDir} with the {@code UserPrefs}'s + * current directory. + */ + private void completeDirName() { + startDir = prefs.getCurrDirectory().toString(); + String commandText = commandTextField.getText(); + if (commandText.trim().equals("")) { + return; + } + + String commandWord = getCommandWord(commandText.trim()); + if (commandWord.equals(CdCommand.COMMAND_WORD)) { + String arguments = getArguments(commandText.trim()); + if ("".equals(arguments)) { + return; + } + searchDirectory(arguments); + } + } + + /** + * Returns command word from given {@code commandText}. + */ + private String getCommandWord(String commandText) { + String[] commandLineArgs = commandText.split(" "); + return commandLineArgs[0]; + } + + /** + * Returns arguments from given {@code commandText}. + */ + private String getArguments(String commandText) { + String[] commandLineArgs = commandText.split(" ", 2); + if (commandLineArgs.length == 1) { + return ""; + } + return commandLineArgs[1]; + } + + /** + * Searches if directories exists in the given {@code arguments}. + */ + private void searchDirectory(String arguments) { + StringBuilder copyArgs = new StringBuilder(); + + String[] directories = arguments.split("/"); + int numDir = directories.length; + + for (int i = 0; i < numDir; i++) { + String dir = directories[i]; + if (i == 0 && Paths.get(dir, "/").isAbsolute()) { + startDir = dir; + copyArgs.append(dir + "/"); + } else if (i == (numDir - 1)) { + // Enters if directory is the last argument. + File checkDir = new File(startDir); + File[] fileList = checkDir.listFiles(); + + if (!dirListWithSearchPrefix.isEmpty() + && dirListWithSearchPrefix.peek().equalsIgnoreCase(dir)) { + dirListWithSearchPrefix.add(dirListWithSearchPrefix.poll()); + + copyArgs.append(dirListWithSearchPrefix.peek()); + String newCommandText = "cd " + copyArgs + "/"; + replaceText(newCommandText); + return; + } + dirListWithSearchPrefix.clear(); + + for (File file : fileList) { + if (file.isDirectory() && file.getName().toUpperCase().startsWith(dir.toUpperCase())) { + dirListWithSearchPrefix.add(file.getName()); + } + } + + if (!dirListWithSearchPrefix.isEmpty()) { + copyArgs.append(dirListWithSearchPrefix.peek()); + String newCommandText = "cd " + copyArgs + "/"; + replaceText(newCommandText); + return; + } + } else { + // checks if directory exists + String newDir = startDir + "/" + dir; + + File checkDir = new File(newDir); + if (checkDir.isDirectory()) { + startDir = newDir; + copyArgs.append(checkDir.getName() + "/"); + } else { + return; + } + } + } + } + //@@author + /** * Handles the Enter button pressed event. */ diff --git a/src/main/java/seedu/address/ui/FilmReel.java b/src/main/java/seedu/address/ui/FilmReel.java new file mode 100644 index 000000000000..c4c018384eaa --- /dev/null +++ b/src/main/java/seedu/address/ui/FilmReel.java @@ -0,0 +1,93 @@ +package seedu.address.ui; + +import java.io.FileNotFoundException; +import java.nio.file.Path; +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.FilmReelSelectionChangeEvent; +import seedu.address.commons.events.ui.UpdateFilmReelEvent; + +//@@author chivent + +/** + * Panel containing the list of images. + */ +public class FilmReel extends UiPart { + private static final String FXML = "FilmReelPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(HistoryListPanel.class); + private ObservableList items = FXCollections.observableArrayList(); + + @FXML + private ListView imageListView; + + public FilmReel() { + super(FXML); + + imageListView.setItems(items); + imageListView.setCellFactory(listView -> new FilmReelCell()); + registerAsAnEventHandler(this); + } + + + /** + * Event that triggers when new images are previewed with next + * + * @param event + */ + @Subscribe + private void handleUpdateFilmReelEvent(UpdateFilmReelEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + items.removeAll(); + items.setAll(event.paths); + + imageListView.scrollTo(0); + imageListView.getSelectionModel().clearSelection(); + } + + /** + * Event that triggers an image is selected with select + * + * @param event + */ + @Subscribe + private void handleFilmReelSelectionChangeEvent(FilmReelSelectionChangeEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + Platform.runLater(() -> { + imageListView.scrollTo(event.index); + imageListView.getSelectionModel().clearAndSelect(event.index); + }); + } + + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Path} using a {@code FilmReelCard}. + */ + class FilmReelCell extends ListCell { + @Override + protected void updateItem(Path image, boolean empty) { + super.updateItem(image, empty); + + if (empty || image == null) { + setGraphic(null); + setText(null); + } else { + try { + setGraphic(new FilmReelCard(image, getIndex() + 1).getRoot()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/FilmReelCard.java b/src/main/java/seedu/address/ui/FilmReelCard.java new file mode 100644 index 000000000000..7c93422ea4b6 --- /dev/null +++ b/src/main/java/seedu/address/ui/FilmReelCard.java @@ -0,0 +1,64 @@ +package seedu.address.ui; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; + +//@@author chivent + +/** + * An UI component that displays information of a selected image. + */ +public class FilmReelCard extends UiPart { + + private static final String FXML = "FilmCard.fxml"; + + /** + * An image panel for showing the previews + */ + private final ObjectProperty image = new SimpleObjectProperty( + new Image("https://via.placeholder.com/500x500")); + + @FXML + private ImageView imagePreview; + @FXML + private Label name; + @FXML + private Label pathName; + + public FilmReelCard(Path path, int displayedIndex) throws FileNotFoundException { + super(FXML); + FileInputStream fis = new FileInputStream(path.toString()); + imagePreview.imageProperty().bind(image); + image.setValue(new Image(fis)); + name.setText(displayedIndex + ""); + pathName.setText(path.toString()); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof FilmReelCard)) { + return false; + } + + // state check + FilmReelCard card = (FilmReelCard) other; + return name.getText().equals(card.name.getText()) + && pathName.getText().equals(card.pathName.getText()); + } +} + diff --git a/src/main/java/seedu/address/ui/HistoryListPanel.java b/src/main/java/seedu/address/ui/HistoryListPanel.java new file mode 100644 index 000000000000..3db0231df197 --- /dev/null +++ b/src/main/java/seedu/address/ui/HistoryListPanel.java @@ -0,0 +1,79 @@ +package seedu.address.ui; + +//@@author chivent +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; + +import javafx.scene.text.Text; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.HistoryUpdateEvent; + + +/** + * Panel containing the list of past transformations. + */ +public class HistoryListPanel extends UiPart { + public static final String SELECTED_STYLE = "-fx-background-color: #a3a3a3; -fx-text-fill: #1f1f1f;"; + + private static final String FXML = "HistoryListPanel.fxml"; + private static final String DEFAULT_STYLE = "-fx-background-color: transparent; -fx-text-fill: #6e6e6e;"; + + private final Logger logger = LogsCenter.getLogger(getClass()); + private ObservableList items = FXCollections.observableArrayList(); + + /** + * Stores transformations that have been undone. + */ + + @FXML + private ListView historyListView; + + @FXML + private Text historyTitle; + + public HistoryListPanel() { + super(FXML); + historyTitle.setText("History of current layer"); + historyListView.setItems(items); + historyListView.setCellFactory(listView -> new HistoryListPanel.HistoryListViewCell()); + registerAsAnEventHandler(this); + } + + @Subscribe + private void handleHistoryUpdateEvent(HistoryUpdateEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + for (int size = items.size(); 0 < size; size--) { + items.remove(items.size() - 1, items.size()); + } + historyListView.setItems(items); + items.addAll(event.list); + historyListView.setItems(items); + } + + /** + * Custom {@code ListCell} that displays transformations. + */ + class HistoryListViewCell extends ListCell { + @Override + protected void updateItem(String transformation, boolean empty) { + super.updateItem(transformation, empty); + + setStyle(DEFAULT_STYLE); + setGraphic(null); + setText(empty ? null : transformation); + + if (!empty && getIndex() == (getListView().getItems().size() - 1)) { + setStyle(SELECTED_STYLE); + } + } + } +} + diff --git a/src/main/java/seedu/address/ui/ImagePanel.java b/src/main/java/seedu/address/ui/ImagePanel.java new file mode 100644 index 000000000000..c97a1d08ff74 --- /dev/null +++ b/src/main/java/seedu/address/ui/ImagePanel.java @@ -0,0 +1,51 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ChangeImageEvent; + +/** + * An UI component that displays an Image. + */ +public class ImagePanel extends UiPart { + + private static final String FXML = "ImagePanel.fxml"; + private final Logger logger = LogsCenter.getLogger(getClass()); + + /** + * An image panel for showing the previews + */ + private final ObjectProperty image = new SimpleObjectProperty( + new Image("https://via.placeholder.com/500x500")); + + private final String name; + + @FXML + private ImageView imageView; + + public ImagePanel(String name) { + super(FXML); + this.name = name; + imageView.imageProperty().bind(image); + registerAsAnEventHandler(this); + } + + @Subscribe + private void handlePreviewImageEvent(ChangeImageEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + if (this.name.equals(event.target)) { + Platform.runLater(() -> image.setValue(event.image)); + } + } + +} diff --git a/src/main/java/seedu/address/ui/LayerListPanel.java b/src/main/java/seedu/address/ui/LayerListPanel.java new file mode 100644 index 000000000000..5953bee60117 --- /dev/null +++ b/src/main/java/seedu/address/ui/LayerListPanel.java @@ -0,0 +1,80 @@ +package seedu.address.ui; + +//@@author j-lum + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; + +import javafx.scene.text.Text; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.LayerUpdateEvent; + + +/** + * Panel containing the list of past transformations. + */ +public class LayerListPanel extends UiPart { + public static final String SELECTED_STYLE = "-fx-background-color: #a3a3a3; -fx-text-fill: #1f1f1f;"; + + private static final String FXML = "LayerListPanel.fxml"; + private static final String DEFAULT_STYLE = "-fx-background-color: transparent; -fx-text-fill: #6e6e6e;"; + + private final Logger logger = LogsCenter.getLogger(getClass()); + private ObservableList items = FXCollections.observableArrayList(); + + @FXML + private ListView layerListView; + + @FXML + private Text layerTitle; + + private Index current = Index.fromZeroBased(0); + + public LayerListPanel() { + super(FXML); + layerTitle.setText("Layers"); + layerListView.setItems(items); + layerListView.setCellFactory(listView -> new LayerListPanel.LayerListViewCell()); + registerAsAnEventHandler(this); + } + + @Subscribe + private void handleLayerUpdateEvent(LayerUpdateEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + this.current = event.current; + for (int size = items.size(); 0 < size; size--) { + items.remove(items.size() - 1, items.size()); + } + layerListView.setItems(items); + items.addAll(event.list); + layerListView.setItems(items); + } + + /** + * Custom {@code ListCell} that displays transformations. + */ + class LayerListViewCell extends ListCell { + @Override + protected void updateItem(String layerName, boolean empty) { + super.updateItem(layerName, empty); + + setStyle(DEFAULT_STYLE); + setGraphic(null); + setText(empty ? null : layerName); + + if (!empty && getIndex() == current.getZeroBased()) { + setStyle(SELECTED_STYLE); + } + } + } +} + diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 0e361a4d7baf..f928dfb941a9 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -20,6 +20,7 @@ import seedu.address.logic.Logic; import seedu.address.model.UserPrefs; + /** * The Main Window. Provides the basic application layout containing * a menu bar and space where other JavaFX elements can be placed. @@ -33,39 +34,53 @@ public class MainWindow extends UiPart { private Stage primaryStage; private Logic logic; + /** + * Identifies logged in google account + */ + private String user; + // Independent Ui parts residing in this Ui container - private BrowserPanel browserPanel; - private PersonListPanel personListPanel; - private Config config; + //private Config config; private UserPrefs prefs; private HelpWindow helpWindow; - @FXML - private StackPane browserPlaceholder; - @FXML private StackPane commandBoxPlaceholder; @FXML private MenuItem helpMenuItem; - @FXML - private StackPane personListPanelPlaceholder; - @FXML private StackPane resultDisplayPlaceholder; @FXML private StackPane statusbarPlaceholder; - public MainWindow(Stage primaryStage, Config config, UserPrefs prefs, Logic logic) { + @FXML + private StackPane originalImagePlaceholder; + + @FXML + private StackPane previewImagePlaceholder; + + @FXML + private StackPane historyListPlaceholder; + + @FXML + private StackPane filmReelPlaceholder; + + @FXML + private StackPane layerListPlaceholder; + + + public MainWindow(Stage primaryStage, Config config, UserPrefs prefs, Logic logic, String user) { super(FXML, primaryStage); // Set dependencies this.primaryStage = primaryStage; this.logic = logic; - this.config = config; + //this.config = config; this.prefs = prefs; + this.user = user; // Configure the UI setTitle(config.getAppTitle()); @@ -87,6 +102,7 @@ private void setAccelerators() { /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -118,24 +134,34 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { /** * Fills up all the placeholders of this window. */ - void fillInnerParts() { - browserPanel = new BrowserPanel(); - browserPlaceholder.getChildren().add(browserPanel.getRoot()); + protected void fillInnerParts() { + + ImagePanel originalImagePanel = new ImagePanel("original"); + originalImagePlaceholder.getChildren().add(originalImagePanel.getRoot()); - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + ImagePanel previewImagePanel = new ImagePanel("preview"); + previewImagePlaceholder.getChildren().add(previewImagePanel.getRoot()); ResultDisplay resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(user, prefs.getCurrDirectory().toString()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(logic); + CommandBox commandBox = new CommandBox(logic, prefs); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + + HistoryListPanel historyList = new HistoryListPanel(); + historyListPlaceholder.getChildren().add(historyList.getRoot()); + + FilmReel filmList = new FilmReel(); + filmReelPlaceholder.getChildren().add(filmList.getRoot()); + + LayerListPanel layerList = new LayerListPanel(); + layerListPlaceholder.getChildren().add(layerList.getRoot()); } - void hide() { + protected void hide() { primaryStage.hide(); } @@ -158,7 +184,7 @@ private void setWindowDefaultSize(UserPrefs prefs) { /** * Returns the current size and the position of the main Window. */ - GuiSettings getCurrentGuiSetting() { + protected GuiSettings getCurrentGuiSetting() { return new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), (int) primaryStage.getX(), (int) primaryStage.getY()); } @@ -175,7 +201,7 @@ public void handleHelp() { } } - void show() { + protected void show() { primaryStage.show(); } @@ -187,14 +213,6 @@ private void handleExit() { raise(new ExitAppRequestEvent()); } - public PersonListPanel getPersonListPanel() { - return personListPanel; - } - - void releaseResources() { - browserPanel.freeResources(); - } - @Subscribe private void handleShowHelpEvent(ShowHelpRequestEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index f6727ea83abd..000000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,70 +0,0 @@ -package seedu.address.ui; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.fxml"; - - /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. - * - * @see The issue on AddressBook level 4 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonCard)) { - return false; - } - - // state check - PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index 80080adb4305..000000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,83 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import com.google.common.eventbus.Subscribe; - -import javafx.application.Platform; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.ui.JumpToListRequestEvent; -import seedu.address.commons.events.ui.PersonPanelSelectionChangedEvent; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - public PersonListPanel(ObservableList personList) { - super(FXML); - setConnections(personList); - registerAsAnEventHandler(this); - } - - private void setConnections(ObservableList personList) { - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - setEventHandlerForSelectionChangeEvent(); - } - - private void setEventHandlerForSelectionChangeEvent() { - personListView.getSelectionModel().selectedItemProperty() - .addListener((observable, oldValue, newValue) -> { - if (newValue != null) { - logger.fine("Selection in person list panel changed to : '" + newValue + "'"); - raise(new PersonPanelSelectionChangedEvent(newValue)); - } - }); - } - - /** - * Scrolls to the {@code PersonCard} at the {@code index} and selects it. - */ - private void scrollTo(int index) { - Platform.runLater(() -> { - personListView.scrollTo(index); - personListView.getSelectionModel().clearAndSelect(index); - }); - } - - @Subscribe - private void handleJumpToListRequestEvent(JumpToListRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - scrollTo(event.targetIndex); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index f6ba29502422..74ee3bdcb1f5 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -1,9 +1,6 @@ package seedu.address.ui; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Clock; -import java.util.Date; +import java.util.Objects; import java.util.logging.Logger; import org.controlsfx.control.StatusBar; @@ -14,70 +11,79 @@ import javafx.fxml.FXML; import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.ui.ChangeDirectoryEvent; +import seedu.address.commons.events.ui.LoginStatusEvent; +import seedu.address.commons.events.ui.LogoutStatusEvent; + +//@@author chivent /** * A ui for the status bar that is displayed at the footer of the application. */ public class StatusBarFooter extends UiPart { - public static final String SYNC_STATUS_INITIAL = "Not updated yet in this session"; - public static final String SYNC_STATUS_UPDATED = "Last Updated: %s"; + public static final String LOGIN_STATUS_INITIAL = "Not connected to Google Photos"; + public static final String LOGIN_STATUS_UPDATED = "Connected to Google Photos as: %s"; + public static final String LOGOUT_MESSAGE = "User logged out"; - /** - * Used to generate time stamps. - * - * TODO: change clock to an instance variable. - * We leave it as a static variable because manual dependency injection - * will require passing down the clock reference all the way from MainApp, - * but it should be easier once we have factories/DI frameworks. - */ - private static Clock clock = Clock.systemDefaultZone(); + public static final String DIRECTORY_ERROR = "Unable to detect directory location"; private static final Logger logger = LogsCenter.getLogger(StatusBarFooter.class); private static final String FXML = "StatusBarFooter.fxml"; @FXML - private StatusBar syncStatus; + private StatusBar loginStatus; + @FXML - private StatusBar saveLocationStatus; + private StatusBar directoryDisplay; - public StatusBarFooter(Path saveLocation) { + public StatusBarFooter(String user, String currentDirectory) { super(FXML); - setSyncStatus(SYNC_STATUS_INITIAL); - setSaveLocation(Paths.get(".").resolve(saveLocation).toString()); + if (user != null) { + setLoginStatus(String.format(LOGIN_STATUS_UPDATED, user)); + } else { + setLoginStatus(LOGIN_STATUS_INITIAL); + } + setDirectoryDisplay(Objects.requireNonNullElse(currentDirectory, DIRECTORY_ERROR)); registerAsAnEventHandler(this); } - /** - * Sets the clock used to determine the current time. - */ - public static void setClock(Clock clock) { - StatusBarFooter.clock = clock; + private void setLoginStatus(String status) { + Platform.runLater(() -> loginStatus.setText(status)); } - /** - * Returns the clock currently in use. - */ - public static Clock getClock() { - return clock; + private void setDirectoryDisplay(String status) { + Platform.runLater(() -> directoryDisplay.setText(status)); } - private void setSaveLocation(String location) { - Platform.runLater(() -> saveLocationStatus.setText(location)); + @Subscribe + public void handleLoginStatusEvent(LoginStatusEvent event) { + if (event.loggedIn) { + setLoginStatus(String.format(LOGIN_STATUS_UPDATED, event.user)); + logger.info(LogsCenter.getEventHandlingLogMessage(event, "User logged in as " + event.user)); + } else { + setLoginStatus(LOGIN_STATUS_INITIAL); + logger.info(LogsCenter.getEventHandlingLogMessage(event, "User is not logged in to google photos")); + } } - private void setSyncStatus(String status) { - Platform.runLater(() -> syncStatus.setText(status)); + @Subscribe + public void handleDirectoryChangeEvent(ChangeDirectoryEvent event) { + if (event.directory.isEmpty()) { + setDirectoryDisplay(DIRECTORY_ERROR); + logger.info(LogsCenter.getEventHandlingLogMessage(event, + "User's current directory location could not be determined")); + } else { + setDirectoryDisplay(event.directory); + logger.info(LogsCenter.getEventHandlingLogMessage(event, "User's current directory: " + event.directory)); + } } @Subscribe - public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { - long now = clock.millis(); - String lastUpdated = new Date(now).toString(); - logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); - setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); + public void handleLogoutStatusEvent(LogoutStatusEvent event) { + setLoginStatus(LOGIN_STATUS_INITIAL); + logger.info(LogsCenter.getEventHandlingLogMessage(event, LOGOUT_MESSAGE)); } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 3fd3c17be156..5c7f8e0d2694 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -30,18 +30,20 @@ public class UiManager extends ComponentManager implements Ui { public static final String FILE_OPS_ERROR_DIALOG_CONTENT_MESSAGE = "Could not save data to file"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/piconso_32.png"; private Logic logic; private Config config; private UserPrefs prefs; private MainWindow mainWindow; + private String user; - public UiManager(Logic logic, Config config, UserPrefs prefs) { + public UiManager(Logic logic, Config config, UserPrefs prefs, String loggedInUser) { super(); this.logic = logic; this.config = config; this.prefs = prefs; + this.user = loggedInUser; } @Override @@ -52,7 +54,7 @@ public void start(Stage primaryStage) { primaryStage.getIcons().add(getImage(ICON_APPLICATION)); try { - mainWindow = new MainWindow(primaryStage, config, prefs, logic); + mainWindow = new MainWindow(primaryStage, config, prefs, logic, user); mainWindow.show(); //This should be called before creating other UI parts mainWindow.fillInnerParts(); @@ -66,7 +68,6 @@ public void start(Stage primaryStage) { public void stop() { prefs.updateLastUsedGuiSetting(mainWindow.getCurrentGuiSetting()); mainWindow.hide(); - mainWindow.releaseResources(); } private void showFileOperationAlertAndWait(String description, String details, Throwable cause) { @@ -78,7 +79,7 @@ private Image getImage(String imagePath) { return new Image(MainApp.class.getResourceAsStream(imagePath)); } - void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { + protected void showAlertDialogAndWait(AlertType type, String title, String headerText, String contentText) { showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); } @@ -104,7 +105,7 @@ private static void showAlertDialogAndWait(Stage owner, AlertType type, String t */ private void showFatalErrorDialogAndShutdown(String title, Throwable e) { logger.severe(title + " " + e.getMessage() + StringUtil.getDetails(e)); - showAlertDialogAndWait(Alert.AlertType.ERROR, title, e.getMessage(), e.toString()); + showAlertDialogAndWait(AlertType.ERROR, title, e.getMessage(), e.toString()); Platform.exit(); System.exit(1); } diff --git a/src/main/resources/client_credentials.json b/src/main/resources/client_credentials.json new file mode 100644 index 000000000000..cdff2f0c8402 --- /dev/null +++ b/src/main/resources/client_credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"872336130854-8amea7m37fnf4bo4919fjul8o1rs1qj6.apps.googleusercontent.com","project_id":"piconso-1537810845766","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://www.googleapis.com/oauth2/v3/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"qunYLt_8e4poCyCIzQ0iUk--","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} diff --git a/src/main/resources/imageMagic/commandTemplate.json b/src/main/resources/imageMagic/commandTemplate.json new file mode 100644 index 000000000000..04917e65335f --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplate.json @@ -0,0 +1 @@ +{"blur":{"num":"1","args":{"arg1":"radius x sigama"}},"contrast":{"num":"0"},"resize":{"num":1,"args":{"arg1":"x%"}},"rotate":{"num":"1","args":{"arg1":"degree"}},"colorspace":{"num":"1","args":{"arg1":"sRGB/RGB/GRAY/LAB/CMYK"}},"sigmoidal-contrast":{"num":"1","args":{"arg1":"15x30%"}}} diff --git a/src/main/resources/imageMagic/commandTemplates/blur.json b/src/main/resources/imageMagic/commandTemplates/blur.json new file mode 100644 index 000000000000..ae98ae1d684a --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/blur.json @@ -0,0 +1 @@ +{"name":"blur","num":"1","args":{"arg1":"radius(0-99)xsigma(0-99)"}, "patterns":{"pattern1":"\\d{1,2}x\\d{1,2}"}} diff --git a/src/main/resources/imageMagic/commandTemplates/colorspace.json b/src/main/resources/imageMagic/commandTemplates/colorspace.json new file mode 100644 index 000000000000..299940e122d6 --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/colorspace.json @@ -0,0 +1 @@ +{"name":"colorspace","num":"1","args":{"arg1":"sRGB/RGB/GRAY/LAB/CMYK"}, "patterns":{"pattern1":"(?i)sRGB|RGB|GRAY|LAB|CMYK"}} diff --git a/src/main/resources/imageMagic/commandTemplates/contrast.json b/src/main/resources/imageMagic/commandTemplates/contrast.json new file mode 100644 index 000000000000..bc8c330a24cb --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/contrast.json @@ -0,0 +1 @@ +{"name":"contrast","num":"0"} diff --git a/src/main/resources/imageMagic/commandTemplates/motion-blur.json b/src/main/resources/imageMagic/commandTemplates/motion-blur.json new file mode 100644 index 000000000000..39aa32fbf158 --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/motion-blur.json @@ -0,0 +1 @@ +{"name":"motion-blur","num":"1","args":{"arg1":"radius(0-29)xsigma(0-19) + angle(0-99)"}, "patterns":{"pattern1":"[1-2]{0,1}\\d{0,1}x[0-1]{0,1}\\d{0,1}\\+\\d{1,2}"}} diff --git a/src/main/resources/imageMagic/commandTemplates/noise.json b/src/main/resources/imageMagic/commandTemplates/noise.json new file mode 100644 index 000000000000..8f7e08da6ec3 --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/noise.json @@ -0,0 +1 @@ +{"name":"noise","num":"1","args":{"arg1":"radius(0-29)"}, "patterns":{"pattern1":"[1-2]{0,1}\\d{0,1}"}} diff --git a/src/main/resources/imageMagic/commandTemplates/resize.json b/src/main/resources/imageMagic/commandTemplates/resize.json new file mode 100644 index 000000000000..13e7b379fb74 --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/resize.json @@ -0,0 +1 @@ +{"name":"resize","num":1,"args":{"arg1":"x%(1% - 399%)"},"patterns":{"pattern1":"[1-9]\\d{0,1}%|[1-3]\\d\\d%"}} diff --git a/src/main/resources/imageMagic/commandTemplates/rotate.json b/src/main/resources/imageMagic/commandTemplates/rotate.json new file mode 100644 index 000000000000..42f30b8f36f3 --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/rotate.json @@ -0,0 +1 @@ +{"name":"rotate","num":"1","args":{"arg1":"degree(-999 - 999)"},"patterns":{"pattern1":"[-]{0,1}\\d{1,3}"}} diff --git a/src/main/resources/imageMagic/commandTemplates/sharpen.json b/src/main/resources/imageMagic/commandTemplates/sharpen.json new file mode 100644 index 000000000000..84e151b1b34a --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/sharpen.json @@ -0,0 +1 @@ +{"name":"sharpen","num":"1","args":{"arg1":"radius(0-29)xsigma(0-29)"}, "patterns":{"pattern1":"[1-2]{0,1}\\d{0,1}x[1-2]{0,1}\\d{0,1}"}} diff --git a/src/main/resources/imageMagic/commandTemplates/sigmoidal-contrast.json b/src/main/resources/imageMagic/commandTemplates/sigmoidal-contrast.json new file mode 100644 index 000000000000..2df60c71a6ff --- /dev/null +++ b/src/main/resources/imageMagic/commandTemplates/sigmoidal-contrast.json @@ -0,0 +1 @@ +{"name":"sigmoidal-contrast","num":"1","args":{"arg1":"contrast(0-99) xmidpoint(0% - 99%)"},"patterns":{"pattern1":"\\d{1,2}x\\d{1,2}%"}} diff --git a/src/main/resources/imageMagic/package/mac/ImageMagick-7.0.8.zip b/src/main/resources/imageMagic/package/mac/ImageMagick-7.0.8.zip new file mode 100644 index 000000000000..d770bce0155c Binary files /dev/null and b/src/main/resources/imageMagic/package/mac/ImageMagick-7.0.8.zip differ diff --git a/src/main/resources/imageMagic/package/win/ImageMagick-7.0.8-14.zip b/src/main/resources/imageMagic/package/win/ImageMagick-7.0.8-14.zip new file mode 100644 index 000000000000..ab921123d63e Binary files /dev/null and b/src/main/resources/imageMagic/package/win/ImageMagick-7.0.8-14.zip differ diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd93..000000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/piconso_32.png b/src/main/resources/images/piconso_32.png new file mode 100644 index 000000000000..69d9729e0d53 Binary files /dev/null and b/src/main/resources/images/piconso_32.png differ diff --git a/src/main/resources/view/BrowserPanel.fxml b/src/main/resources/view/BrowserPanel.fxml deleted file mode 100644 index 31670827e3da..000000000000 --- a/src/main/resources/view/BrowserPanel.fxml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index c8941ea18263..f0e63ef0e151 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -6,7 +6,6 @@ .label { -fx-font-size: 11pt; -fx-font-family: "Segoe UI Semibold"; - -fx-text-fill: #555555; -fx-opacity: 0.9; } @@ -93,43 +92,37 @@ -fx-background-color: derive(#1d1d1d, 20%); } -.list-cell { - -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; -} - -.list-cell:filled:even { - -fx-background-color: #3c3e3f; -} - -.list-cell:filled:odd { - -fx-background-color: #515658; +#historyPanel { + -fx-padding: 5 5 5 5; } -.list-cell:filled:selected { - -fx-background-color: #424d5f; +#historyListView .list-cell, #imageListView .list-cell { + -fx-label-padding: 1 1 1 1; + -fx-graphic-text-gap : 0; + -fx-padding: 1 1 1 1; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-background-color: transparent; } -.list-cell:filled:selected #cardPane { - -fx-border-color: #3e7b91; - -fx-border-width: 1; +#historyListView .list-cell:filled:selected { + -fx-background-color: #BDBBBA; + -fx-text-fill: black; } -.list-cell .label { +#historyListView .list-cell:filled:focused { + -fx-background-color: transparent; -fx-text-fill: white; } -.cell_big_label { - -fx-font-family: "Segoe UI Semibold"; - -fx-font-size: 16px; - -fx-text-fill: #010504; +#imageListView .list-cell:filled:selected { + -fx-background-color: #525252; + -fx-text-fill: #1F1F1F; } -.cell_small_label { - -fx-font-family: "Segoe UI"; +.cell_big_label { -fx-font-size: 13px; - -fx-text-fill: #010504; + -fx-text-fill: #d9d7d6; } .stack-pane { @@ -154,10 +147,6 @@ -fx-text-fill: white; } -.result-display .label { - -fx-text-fill: black !important; -} - .status-bar .label { -fx-font-family: "Segoe UI Light"; -fx-text-fill: white; @@ -183,29 +172,6 @@ -fx-background-color: derive(#1d1d1d, 30%); } -.context-menu { - -fx-background-color: derive(#1d1d1d, 50%); -} - -.context-menu .label { - -fx-text-fill: white; -} - -.menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); -} - -.menu-bar .label { - -fx-font-size: 14pt; - -fx-font-family: "Segoe UI Light"; - -fx-text-fill: white; - -fx-opacity: 0.9; -} - -.menu .left-container { - -fx-background-color: black; -} - /* * Metro style Push Button * Author: Pedro Duque Vieira @@ -327,25 +293,13 @@ -fx-text-fill: white; } -#filterField, #personListPanel, #personWebpage { - -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); -} - #resultDisplay .content { -fx-background-color: transparent, #383838, transparent, #383838; -fx-background-radius: 0; } -#tags { - -fx-hgap: 7; - -fx-vgap: 3; -} - -#tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; - -fx-padding: 1 3 1 3; - -fx-border-radius: 2; - -fx-background-radius: 2; - -fx-font-size: 11; +.titles { + -fx-text-fill: #d9d7d6; + -fx-fill: #d9d7d6; + -fx-font-size: 15px; } diff --git a/src/main/resources/view/FilmCard.fxml b/src/main/resources/view/FilmCard.fxml new file mode 100644 index 000000000000..73b4892146e1 --- /dev/null +++ b/src/main/resources/view/FilmCard.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/FilmReelPanel.fxml b/src/main/resources/view/FilmReelPanel.fxml new file mode 100644 index 000000000000..9fc83f60e9a3 --- /dev/null +++ b/src/main/resources/view/FilmReelPanel.fxml @@ -0,0 +1,18 @@ + + + + + + + + + + + Images in directory batch + + 7 + + + + + diff --git a/src/main/resources/view/HistoryListPanel.fxml b/src/main/resources/view/HistoryListPanel.fxml new file mode 100644 index 000000000000..8774e744920e --- /dev/null +++ b/src/main/resources/view/HistoryListPanel.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + History List + + + 7 + + + + + diff --git a/src/main/resources/view/ImagePanel.fxml b/src/main/resources/view/ImagePanel.fxml new file mode 100644 index 000000000000..b089377d5362 --- /dev/null +++ b/src/main/resources/view/ImagePanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/LayerListPanel.fxml b/src/main/resources/view/LayerListPanel.fxml new file mode 100644 index 000000000000..6e67338850c3 --- /dev/null +++ b/src/main/resources/view/LayerListPanel.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + Layers + + + 7 + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index daf386d8f5b8..c11355bb2ac0 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -11,58 +11,92 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + minWidth="1280" minHeight="800" onCloseRequest="#handleExit"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml deleted file mode 100644 index f08ea32ad558..000000000000 --- a/src/main/resources/view/PersonListCard.fxml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml deleted file mode 100644 index 8836d323cc5d..000000000000 --- a/src/main/resources/view/PersonListPanel.fxml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/view/StatusBarFooter.fxml b/src/main/resources/view/StatusBarFooter.fxml index 041e1ff9004f..a01ab74240fd 100644 --- a/src/main/resources/view/StatusBarFooter.fxml +++ b/src/main/resources/view/StatusBarFooter.fxml @@ -9,6 +9,6 @@ - - + + diff --git a/src/test/data/JsonConvertArgsStorageTest/blurR.json b/src/test/data/JsonConvertArgsStorageTest/blurR.json new file mode 100644 index 000000000000..4c5987a6f895 --- /dev/null +++ b/src/test/data/JsonConvertArgsStorageTest/blurR.json @@ -0,0 +1 @@ +{"name":"blurR","num":2,"operations":{"op1":{"name":"blur","num":1,"args":{"arg1":"0x8"}},"op2":{"name":"rotate","num":1,"args":{"arg1":"90"}}}} diff --git a/src/test/data/JsonConvertArgsStorageTest/newOperation.json b/src/test/data/JsonConvertArgsStorageTest/newOperation.json new file mode 100644 index 000000000000..163e22317d88 --- /dev/null +++ b/src/test/data/JsonConvertArgsStorageTest/newOperation.json @@ -0,0 +1 @@ +{"name":"newOperation","num":2,"operations":{"op1":{"name":"blur","num":1,"args":{"arg1":"0x8"}},"op2":{"name":"blur","num":1,"args":{"arg1":"0x8"}}}} diff --git a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json index a312cecf8bad..a6c0172e9f72 100644 --- a/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/ExtraValuesUserPref.json @@ -8,6 +8,5 @@ "y" : 100, "z" : 99 } - }, - "addressBookFilePath" : "addressbook.xml" + } } diff --git a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json index 412dbd7cac65..d37cd8a65140 100644 --- a/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json +++ b/src/test/data/JsonUserPrefsStorageTest/TypicalUserPref.json @@ -6,6 +6,5 @@ "x" : 300, "y" : 100 } - }, - "addressBookFilePath" : "addressbook.xml" + } } diff --git a/src/test/data/XmlAddressBookStorageTest/NotXmlFormatAddressBook.xml b/src/test/data/XmlAddressBookStorageTest/NotXmlFormatAddressBook.xml deleted file mode 100644 index 35efc0f50820..000000000000 --- a/src/test/data/XmlAddressBookStorageTest/NotXmlFormatAddressBook.xml +++ /dev/null @@ -1 +0,0 @@ -not xml format! diff --git a/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml b/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml deleted file mode 100644 index 41e411568a5f..000000000000 --- a/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Hans Muster - 9482424 - hans@example.com -
4th street
-
- - - Hans Muster - 948asdf2424 - hans@example.com -
4th street
-
-
diff --git a/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml b/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml deleted file mode 100644 index cfa128e72828..000000000000 --- a/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Ha!ns Mu@ster - 9482424 - hans@example.com -
4th street
-
-
diff --git a/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml b/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml deleted file mode 100644 index ac02230263d3..000000000000 --- a/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Alice Pauline - 94351253 - alice@example.com -
123, Jurong West Ave 6, #08-111
- friends -
- - - - Alice Pauline - 94351253 - pauline@example.com -
4th street
-
- -
diff --git a/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml b/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml deleted file mode 100644 index 13d5b1cb1c8a..000000000000 --- a/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Hans Muster - 9482424 - hans@exam!32ple -
4th street
-
-
diff --git a/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml b/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml deleted file mode 100644 index d812b05e32bb..000000000000 --- a/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - Alice Pauline - 94351253 - alice@example.com -
123, Jurong West Ave 6, #08-111
- friends -
- - Benson Meier - 98765432 - johnd@example.com -
311, Clementi Ave 2, #02-25
- owesMoney - friends -
- - Carl Kurz - 95352563 - heinz@example.com -
wall street
-
- - Daniel Meier - 87652533 - cornelia@example.com -
10th street
- friends -
- - Elle Meyer - 9482224 - werner@example.com -
michegan ave
-
- - Fiona Kunz - 9482427 - lydia@example.com -
little tokyo
-
- - George Best - 9482442 - anna@example.com -
4th street
-
-
diff --git a/src/test/data/XmlUtilTest/invalidPersonField.xml b/src/test/data/XmlUtilTest/invalidPersonField.xml deleted file mode 100644 index ba49c971e884..000000000000 --- a/src/test/data/XmlUtilTest/invalidPersonField.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - Hans Muster - 9482asf424 - hans@example -
4th street
- friends -
diff --git a/src/test/data/XmlUtilTest/missingPersonField.xml b/src/test/data/XmlUtilTest/missingPersonField.xml deleted file mode 100644 index c0da5c86d080..000000000000 --- a/src/test/data/XmlUtilTest/missingPersonField.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - 9482424 - hans@example -
4th street
- friends -
diff --git a/src/test/data/XmlUtilTest/tempAddressBook.xml b/src/test/data/XmlUtilTest/tempAddressBook.xml deleted file mode 100644 index 4773cf598f4b..000000000000 --- a/src/test/data/XmlUtilTest/tempAddressBook.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - 1 - John - Doe - - - - - - diff --git a/src/test/data/XmlUtilTest/validAddressBook.xml b/src/test/data/XmlUtilTest/validAddressBook.xml deleted file mode 100644 index 6265778674d3..000000000000 --- a/src/test/data/XmlUtilTest/validAddressBook.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - Hans Muster - 9482424 - hans@example.com -
4th street
-
- - Ruth Mueller - 87249245 - ruth@example.com -
81th street
-
- - Heinz Kurz - 95352563 - heinz@example.com -
wall street
-
- - Cornelia Meier - 87652533 - cornelia@example.com -
10th street
-
- - Werner Meyer - 9482224 - werner@example.com -
michegan ave
-
- - Lydia Kunz - 9482427 - lydia@example.com -
little tokyo
-
- - Anna Best - 9482442 - anna@example.com -
4th street
-
- - Stefan Meier - 8482424 - stefan@example.com -
little india
-
- - Martin Mueller - 8482131 - hans@example.com -
chicago ave
-
-
diff --git a/src/test/data/XmlUtilTest/validPerson.xml b/src/test/data/XmlUtilTest/validPerson.xml deleted file mode 100644 index c029008d54f4..000000000000 --- a/src/test/data/XmlUtilTest/validPerson.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Hans Muster - 9482424 - hans@example -
4th street
- friends -
diff --git a/src/test/data/sandbox/testFolder.zip b/src/test/data/sandbox/testFolder.zip new file mode 100644 index 000000000000..bb2a16ab951f Binary files /dev/null and b/src/test/data/sandbox/testFolder.zip differ diff --git a/src/test/java/guitests/guihandles/BrowserPanelHandle.java b/src/test/java/guitests/guihandles/BrowserPanelHandle.java deleted file mode 100644 index bd3633af78f3..000000000000 --- a/src/test/java/guitests/guihandles/BrowserPanelHandle.java +++ /dev/null @@ -1,64 +0,0 @@ -package guitests.guihandles; - -import java.net.URL; - -import guitests.GuiRobot; -import javafx.concurrent.Worker; -import javafx.scene.Node; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; - -/** - * A handler for the {@code BrowserPanel} of the UI. - */ -public class BrowserPanelHandle extends NodeHandle { - - public static final String BROWSER_ID = "#browser"; - - private boolean isWebViewLoaded = true; - - private URL lastRememberedUrl; - - public BrowserPanelHandle(Node browserPanelNode) { - super(browserPanelNode); - - WebView webView = getChildNode(BROWSER_ID); - WebEngine engine = webView.getEngine(); - new GuiRobot().interact(() -> engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { - if (newState == Worker.State.RUNNING) { - isWebViewLoaded = false; - } else if (newState == Worker.State.SUCCEEDED) { - isWebViewLoaded = true; - } - })); - } - - /** - * Returns the {@code URL} of the currently loaded page. - */ - public URL getLoadedUrl() { - return WebViewUtil.getLoadedUrl(getChildNode(BROWSER_ID)); - } - - /** - * Remembers the {@code URL} of the currently loaded page. - */ - public void rememberUrl() { - lastRememberedUrl = getLoadedUrl(); - } - - /** - * Returns true if the current {@code URL} is different from the value remembered by the most recent - * {@code rememberUrl()} call. - */ - public boolean isUrlChanged() { - return !lastRememberedUrl.equals(getLoadedUrl()); - } - - /** - * Returns true if the browser is done loading a page, or if this browser has yet to load any page. - */ - public boolean isLoaded() { - return isWebViewLoaded; - } -} diff --git a/src/test/java/guitests/guihandles/CommandBoxHandle.java b/src/test/java/guitests/guihandles/CommandBoxHandle.java index aa59b1e470e2..66ff1f71af5a 100644 --- a/src/test/java/guitests/guihandles/CommandBoxHandle.java +++ b/src/test/java/guitests/guihandles/CommandBoxHandle.java @@ -22,6 +22,15 @@ public String getInput() { return getRootNode().getText(); } + /** + * Sets the text in the command box for testing. + */ + public void setText(String text) { + click(); + guiRobot.interact(() -> getRootNode().setText(text)); + guiRobot.pauseForHuman(); + } + /** * Enters the given command in the Command Box and presses enter. */ diff --git a/src/test/java/guitests/guihandles/FilmCardHandle.java b/src/test/java/guitests/guihandles/FilmCardHandle.java new file mode 100644 index 000000000000..de85d9ba84e6 --- /dev/null +++ b/src/test/java/guitests/guihandles/FilmCardHandle.java @@ -0,0 +1,45 @@ +package guitests.guihandles; + +import java.nio.file.Path; + +import javafx.scene.Node; +import javafx.scene.control.Label; + +/** + * Handle to {@code FilmReelCard}. + */ +public class FilmCardHandle extends NodeHandle { + + private static final String PATH_ID = "#pathName"; + private static final String LABEL_ID = "#name"; + + private final Label pathName; + private final Label name; + + public FilmCardHandle(Node filmReelCard) { + super(filmReelCard); + pathName = getChildNode(PATH_ID); + name = getChildNode(LABEL_ID); + } + + public String getTitle() { + return name.getText(); + } + + public String getPath() { + return pathName.getText(); + } + + /** + * Returns true if this handle contains {@code Path}. + */ + public boolean equals(Path path, int index) { + return getTitle().equals(Integer.toString(index + 1)) + && getPath().equals(path.toString()); + } + + @Override + public String toString() { + return pathName.getText() + "" + name.getText(); + } +} diff --git a/src/test/java/guitests/guihandles/FilmReelHandle.java b/src/test/java/guitests/guihandles/FilmReelHandle.java new file mode 100644 index 000000000000..33dd413cba7a --- /dev/null +++ b/src/test/java/guitests/guihandles/FilmReelHandle.java @@ -0,0 +1,60 @@ +package guitests.guihandles; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; + +import javafx.scene.Node; +import javafx.scene.control.ListView; + +/** + * Provides a handle to {@code FilmReel}. + */ +public class FilmReelHandle extends NodeHandle { + + public static final String IMAGE_LIST_ID = "#imageListView"; + private static final String CARD_PANE_ID = "#cardPane"; + + private final ListView imageListView; + + public FilmReelHandle(Node filmReel) { + super(filmReel); + imageListView = getChildNode(IMAGE_LIST_ID); + } + + private Path getPath(int index) { + return imageListView.getItems().get(index); + } + + /** + * Returns a handle to the selected {@code FilmCardHandle}. + * A maximum of 1 item can be selected at any time. + */ + public Optional getHandleToSelectedCard() { + int index = imageListView.getSelectionModel().getSelectedIndex(); + System.out.println(index); + return index >= 0 ? getFilmCardHandle(index) : Optional.empty(); + } + + /** + * Returns a handle to a {@code FilmCard}. + */ + public Optional getFilmCardHandle(int i) { + return getAllCardNodes().stream() + .map(FilmCardHandle::new) + .filter(handle -> handle.equals(getPath(i), i)) + .findFirst(); + + } + + /** + * Returns all card nodes in the scene graph. + */ + private Set getAllCardNodes() { + return guiRobot.lookup(CARD_PANE_ID).queryAll(); + } + + public int getSize() { + return imageListView.getItems().size(); + } +} diff --git a/src/test/java/guitests/guihandles/HistoryListPanelHandle.java b/src/test/java/guitests/guihandles/HistoryListPanelHandle.java new file mode 100644 index 000000000000..dabcc67158eb --- /dev/null +++ b/src/test/java/guitests/guihandles/HistoryListPanelHandle.java @@ -0,0 +1,26 @@ +package guitests.guihandles; + +//@@author chivent +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.ListView; +/** + * Provides a handle to {@code HistoryListPanel}. + */ +public class HistoryListPanelHandle extends NodeHandle { + public static final String HISTORY_LIST_VIEW_ID = "#historyListView"; + private final ListView historyListView; + + public HistoryListPanelHandle(Node historyListPanel) { + super(historyListPanel); + historyListView = getChildNode(HISTORY_LIST_VIEW_ID); + } + + /** + * Returns the items in {@code HistoryListView}. + */ + public ObservableList getItems() { + return historyListView.getItems(); + } + +} diff --git a/src/test/java/guitests/guihandles/LayerListPanelHandle.java b/src/test/java/guitests/guihandles/LayerListPanelHandle.java new file mode 100644 index 000000000000..bb00e8d2f2e3 --- /dev/null +++ b/src/test/java/guitests/guihandles/LayerListPanelHandle.java @@ -0,0 +1,26 @@ +package guitests.guihandles; + +//@@author j-lum +import javafx.collections.ObservableList; +import javafx.scene.Node; +import javafx.scene.control.ListView; +/** + * Provides a handle to {@code LayerListPanel}. + */ +public class LayerListPanelHandle extends NodeHandle { + public static final String HISTORY_LIST_VIEW_ID = "#layerListView"; + private final ListView layerListView; + + public LayerListPanelHandle(Node historyListPanel) { + super(historyListPanel); + layerListView = getChildNode(HISTORY_LIST_VIEW_ID); + } + + /** + * Returns the items in {@code LayerListView}. + */ + public ObservableList getItems() { + return layerListView.getItems(); + } + +} diff --git a/src/test/java/guitests/guihandles/MainWindowHandle.java b/src/test/java/guitests/guihandles/MainWindowHandle.java index 34e36054f4fd..6e546c333949 100644 --- a/src/test/java/guitests/guihandles/MainWindowHandle.java +++ b/src/test/java/guitests/guihandles/MainWindowHandle.java @@ -7,26 +7,22 @@ */ public class MainWindowHandle extends StageHandle { - private final PersonListPanelHandle personListPanel; private final ResultDisplayHandle resultDisplay; private final CommandBoxHandle commandBox; private final StatusBarFooterHandle statusBarFooter; private final MainMenuHandle mainMenu; - private final BrowserPanelHandle browserPanel; + private final FilmReelHandle filmReel; + private final HistoryListPanelHandle historyPanel; public MainWindowHandle(Stage stage) { super(stage); - personListPanel = new PersonListPanelHandle(getChildNode(PersonListPanelHandle.PERSON_LIST_VIEW_ID)); resultDisplay = new ResultDisplayHandle(getChildNode(ResultDisplayHandle.RESULT_DISPLAY_ID)); commandBox = new CommandBoxHandle(getChildNode(CommandBoxHandle.COMMAND_INPUT_FIELD_ID)); statusBarFooter = new StatusBarFooterHandle(getChildNode(StatusBarFooterHandle.STATUS_BAR_PLACEHOLDER)); mainMenu = new MainMenuHandle(getChildNode(MainMenuHandle.MENU_BAR_ID)); - browserPanel = new BrowserPanelHandle(getChildNode(BrowserPanelHandle.BROWSER_ID)); - } - - public PersonListPanelHandle getPersonListPanel() { - return personListPanel; + filmReel = new FilmReelHandle(getChildNode(FilmReelHandle.IMAGE_LIST_ID)); + historyPanel = new HistoryListPanelHandle(getChildNode(HistoryListPanelHandle.HISTORY_LIST_VIEW_ID)); } public ResultDisplayHandle getResultDisplay() { @@ -45,7 +41,11 @@ public MainMenuHandle getMainMenu() { return mainMenu; } - public BrowserPanelHandle getBrowserPanel() { - return browserPanel; + public FilmReelHandle getFilmReel() { + return filmReel; + } + + public HistoryListPanelHandle getHistoryPanel() { + return historyPanel; } } diff --git a/src/test/java/guitests/guihandles/PersonCardHandle.java b/src/test/java/guitests/guihandles/PersonCardHandle.java deleted file mode 100644 index 1789735e49a8..000000000000 --- a/src/test/java/guitests/guihandles/PersonCardHandle.java +++ /dev/null @@ -1,87 +0,0 @@ -package guitests.guihandles; - -import java.util.List; -import java.util.stream.Collectors; - -import com.google.common.collect.ImmutableMultiset; - -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * Provides a handle to a person card in the person list panel. - */ -public class PersonCardHandle extends NodeHandle { - private static final String ID_FIELD_ID = "#id"; - private static final String NAME_FIELD_ID = "#name"; - private static final String ADDRESS_FIELD_ID = "#address"; - private static final String PHONE_FIELD_ID = "#phone"; - private static final String EMAIL_FIELD_ID = "#email"; - private static final String TAGS_FIELD_ID = "#tags"; - - private final Label idLabel; - private final Label nameLabel; - private final Label addressLabel; - private final Label phoneLabel; - private final Label emailLabel; - private final List