From 1b9b575d8cd7fc176a9d0d8d4acf31084e1cbeda Mon Sep 17 00:00:00 2001 From: tanhengyeow Date: Sun, 15 Apr 2018 21:38:29 +0800 Subject: [PATCH] Updated UGDG & collate final code --- collated/functional/Ang-YC.md | 4896 ++++++++++++++-------------- collated/functional/kexiaowen.md | 608 ++-- collated/functional/mhq199657.md | 2218 ++++++------- collated/functional/tanhengyeow.md | 3015 ++++++++--------- collated/test/Ang-YC.md | 1332 +++++--- collated/test/kexiaowen.md | 1786 +++++----- collated/test/mhq199657.md | 720 ++-- collated/test/tanhengyeow.md | 2345 +++++++------ docs/DeveloperGuide.adoc | 21 +- docs/UserGuide.adoc | 40 +- docs/team/tanhengyeow.adoc | 16 +- 11 files changed, 8906 insertions(+), 8091 deletions(-) diff --git a/collated/functional/Ang-YC.md b/collated/functional/Ang-YC.md index e7fedcf7f848..831b6f36996e 100644 --- a/collated/functional/Ang-YC.md +++ b/collated/functional/Ang-YC.md @@ -1,1316 +1,1204 @@ # Ang-YC -###### /java/seedu/address/commons/events/ui/InfoPanelChangedEvent.java +###### /java/seedu/address/ui/UiResizer.java ``` java /** - * Indicates a change in Info Panel (Used for automated testing purpose) + * Ui Resizer, a utility to manage resize event of Stage such as resizable window */ -public class InfoPanelChangedEvent extends Event { - public static final EventType INFO_PANEL_EVENT = - new EventType<>("InfoPanelChangedEvent"); +public class UiResizer { - public InfoPanelChangedEvent() { - this(INFO_PANEL_EVENT); - } + private Stage stage; - public InfoPanelChangedEvent(EventType eventType) { - super(eventType); - } -} -``` -###### /java/seedu/address/commons/events/ui/MaximizeAppRequestEvent.java -``` java -/** - * Indicates a request for App minimize - */ -public class MaximizeAppRequestEvent extends BaseEvent { + private double lastX; + private double lastY; + private double lastWidth; + private double lastHeight; - @Override - public String toString() { - return this.getClass().getSimpleName(); + public UiResizer(Stage stage, GuiSettings guiSettings, double minWidth, double minHeight, int cornerSize) { + this.stage = stage; + + // Set listeners + ResizeListener resizeListener = new ResizeListener(stage, minWidth, minHeight, cornerSize); + stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener); + stage.getScene().addEventHandler(MouseEvent.MOUSE_RELEASED, resizeListener); + + // Set last value + lastX = guiSettings.getWindowCoordinates().x; + lastY = guiSettings.getWindowCoordinates().y; + lastWidth = guiSettings.getWindowWidth(); + lastHeight = guiSettings.getWindowHeight(); } -} -``` -###### /java/seedu/address/commons/events/ui/MinimizeAppRequestEvent.java -``` java -/** - * Indicates a request for App minimize - */ -public class MinimizeAppRequestEvent extends BaseEvent { - @Override - public String toString() { - return this.getClass().getSimpleName(); + private Rectangle2D getScreenBound() { + return Screen.getPrimary().getVisualBounds(); } -} -``` -###### /java/seedu/address/commons/events/ui/PersonChangedEvent.java -``` java -/** - * Indicates a person change in address book - */ -public class PersonChangedEvent extends BaseEvent { - private final Person source; - private final Person target; + /** + * Maximize / Un-maximize the stage, polyfill for native {@link Stage#setMaximized} feature + */ + public void toggleMaximize() { + Rectangle2D screenBound = getScreenBound(); + double stageX = stage.getX(); + double stageY = stage.getY(); + double stageWidth = stage.getWidth(); + double stageHeight = stage.getHeight(); - public PersonChangedEvent(Person source, Person target) { - this.source = source; - this.target = target; + if (stageWidth == screenBound.getWidth() && stageHeight == screenBound.getHeight()) { + stage.setX(lastX); + stage.setY(lastY); + stage.setWidth(lastWidth); + stage.setHeight(lastHeight); + } else { + lastX = stageX; + lastY = stageY; + lastWidth = stageWidth; + lastHeight = stageHeight; + stage.setX(screenBound.getMinX()); + stage.setY(screenBound.getMinY()); + stage.setWidth(screenBound.getWidth()); + stage.setHeight(screenBound.getHeight()); + } } - public Person getSource() { - return source; - } + /** + * Manage the resize event during mouse move and drag + */ + static class ResizeListener implements EventHandler { + private Stage stage; - public Person getTarget() { - return target; - } + private boolean holding = false; + private int cornerSize; - @Override - public String toString() { - return this.getClass().getSimpleName(); - } -} -``` -###### /java/seedu/address/commons/events/ui/ShowPanelRequestEvent.java -``` java -/** - * Indicates a request for panel show - */ -public class ShowPanelRequestEvent extends BaseEvent { + // Starting position of resizing + private double startX = 0; + private double startY = 0; - private final String panel; + // Min sizes for stage + private double minWidth; + private double minHeight; - public ShowPanelRequestEvent(String panel) { - this.panel = panel; - } + public ResizeListener(Stage stage, double minWidth, double minHeight, int borderSize) { + this.stage = stage; + this.minWidth = minWidth; + this.minHeight = minHeight; + this.cornerSize = borderSize; + } - public String getRequestedPanel() { - return panel; - } + @Override + public void handle(MouseEvent mouseEvent) { + String eventType = mouseEvent.getEventType().getName(); + Scene scene = stage.getScene(); + + double mouseX = mouseEvent.getSceneX(); + double mouseY = mouseEvent.getSceneY(); + + + switch (eventType) { + + case "MOUSE_MOVED": + scene.setCursor((isResizePosition(mouseX, mouseY) || holding) ? Cursor.SE_RESIZE : Cursor.DEFAULT); + break; + + case "MOUSE_RELEASED": + holding = false; + scene.setCursor(Cursor.DEFAULT); + break; + + case "MOUSE_PRESSED": + // Left click only + if (MouseButton.PRIMARY.equals(mouseEvent.getButton())) { + holding = isResizePosition(mouseX, mouseY); + startX = stage.getWidth() - mouseX; + startY = stage.getHeight() - mouseY; + } + break; + + case "MOUSE_DRAGGED": + if (holding) { + setStageWidth(mouseX + startX); + setStageHeight(mouseY + startY); + } + break; + + default: + + } + } + + /** + * Check if the X and Y coordinate of the mouse are in the range of draggable position + * + * @param x coordinate of the {@code MouseEvent} + * @param y coordinate of the {@code MouseEvent} + * @return {@code true} if the coordinate is in the range of draggable position, {@code false} otherwise + */ + private boolean isResizePosition(double x, double y) { + Scene scene = stage.getScene(); + return (x > scene.getWidth() - cornerSize && y > scene.getHeight() - cornerSize); + } + + /** + * Set the width of the stage, with validation to be larger than {@code minWidth} + * + * @param width of the stage + */ + private void setStageWidth(double width) { + stage.setWidth(Math.max(width, minWidth)); + } + + /** + * Set the height of the stage, with validation to be larger than {@code minHeight} + * + * @param height of the stage + */ + private void setStageHeight(double height) { + stage.setHeight(Math.max(height, minHeight)); + } - @Override - public String toString() { - return this.getClass().getSimpleName(); } } ``` -###### /java/seedu/address/commons/util/UiUtil.java +###### /java/seedu/address/ui/TitleBar.java ``` java -/** - * Helper functions for handling UI information - */ -public class UiUtil { - public static final Interpolator EASE_OUT_CUBIC = Interpolator.SPLINE(0.215, 0.61, 0.355, 1); - private static final String FORMAT_DATE = "d MMM y"; - private static final String FORMAT_TIME = "hh:mm:ssa"; - /** - * Convert double into string with {@code points} amount of decimal places - * @param decimal The double to be formatted - * @param points Number of decimal places - * @return the formatted string with {@code points} number of decimal places + * Opens the help window. */ - public static String toFixed(double decimal, int points) { - return toFixed(String.valueOf(decimal), points); + @FXML + public void handleHelp(MouseEvent event) { + if (MouseButton.PRIMARY.equals(event.getButton())) { + HelpWindow helpWindow = new HelpWindow(); + helpWindow.show(); + } } /** - * Convert string representation of decimal into string with {@code points} amount of decimal places - * @param decimal The string representation of decimal to be formatted - * @param points Number of decimal places - * @return the formatted string with {@code points} number of decimal places + * Minimizes the application. */ - public static String toFixed(String decimal, int points) { - double value = Double.parseDouble(decimal); - String pattern = "0"; - - if (points > 0) { - pattern += "."; - pattern += StringUtils.repeat("0", points); + @FXML + private void handleMinimize(MouseEvent event) { + if (MouseButton.PRIMARY.equals(event.getButton())) { + raise(new MinimizeAppRequestEvent()); } - - DecimalFormat df = new DecimalFormat(pattern); - return df.format(value); } /** - * Convert JavaFX color into web hex color - * @param color to be converted - * @return the web hex String representation of the color + * Maximizes the application. */ - public static String colorToHex(Color color) { - return String.format("#%02X%02X%02X", - (int) (color.getRed() * 255), (int) (color.getGreen() * 255), (int) (color.getBlue() * 255)); + @FXML + private void handleMaximize(MouseEvent event) { + if (MouseButton.PRIMARY.equals(event.getButton())) { + raise(new MaximizeAppRequestEvent()); + } } /** - * Fade in or fade out the node, then callback - * @param node to be faded in or out - * @param fadeIn If set, the fade will be fade in, otherwise it will be fade out - * @param from The opacity to start fading from - * @param maxDuration of the transition should be - * @param callback after the transition is done - * @return the {@code Animation} of the transition + * Closes the application. */ - public static Animation fadeNode(Node node, boolean fadeIn, double from, - double maxDuration, EventHandler callback) { - Interpolator easing = fadeIn ? Interpolator.EASE_IN : Interpolator.EASE_OUT; - double to = fadeIn ? 1 : 0; - double duration = Math.max(1, Math.abs(from - to) * maxDuration); - - FadeTransition fade = new FadeTransition(Duration.millis(duration), node); - fade.setFromValue(from); - fade.setToValue(to); - fade.setCycleCount(1); - fade.setAutoReverse(false); - fade.setInterpolator(easing); - fade.setOnFinished(event -> { - if (Math.abs(node.getOpacity() - to) < 1e-3) { - callback.handle(event); - } - }); - - return fade; + @FXML + private void handleExit(MouseEvent event) { + if (MouseButton.PRIMARY.equals(event.getButton())) { + raise(new ExitAppRequestEvent()); + } } - /** - * Fade in or fade out the node, then callback - * @param node to be faded in or out - * @param fadeIn If set, the fade will be fade in, otherwise it will be fade out - * @param maxDuration of the transition should be - * @param callback after the transition is done - * @return the {@code Animation} of the transition - */ - public static Animation fadeNode(Node node, boolean fadeIn, - double maxDuration, EventHandler callback) { - double from = node.getOpacity(); - return fadeNode(node, fadeIn, from, maxDuration, callback); + @Subscribe + private void handleShowHelpEvent(ShowHelpRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + HelpWindow helpWindow = new HelpWindow(); + helpWindow.show(); } - /** - * Format date time to more readable format - * @param dateTime to be formatted - * @return the formatted date time - */ - public static String formatDate(LocalDateTime dateTime) { - String date = DateTimeFormatter.ofPattern(FORMAT_DATE, Locale.ENGLISH).format(dateTime); - String time = DateTimeFormatter.ofPattern(FORMAT_TIME, Locale.ENGLISH).format(dateTime).toLowerCase(); - return date + " " + time; + @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)); } } ``` -###### /java/seedu/address/logic/commands/InterviewCommand.java +###### /java/seedu/address/ui/InfoPanel.java ``` java /** - * Schedule interview of an existing person in the address book. + * The Info Panel of the App. */ -public class InterviewCommand extends UndoableCommand { +public class InfoPanel extends UiPart { - public static final String COMMAND_WORD = "interview"; + public static final Person DEFAULT_PERSON = null; + public static final int SPLIT_MIN_WIDTH = 550; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Schedule interview for the person " - + "by the index number used in the last person listing. " - + "Existing scheduled date will be overwritten by the input value.\n" - + "Parameters: INDEX (must be a positive integer) " - + "DATETIME (parse by natural language)\n" - + "Example: " + COMMAND_WORD + " 1 next Friday at 3pm"; + public static final String PANEL_NAME = "InfoPanel"; + private static final String FXML = "InfoPanel.fxml"; + private static final double MAX_ANIMATION_TIME_MS = 150; + private static final double ANIMATION_DELAY_MS = 15; - public static final String MESSAGE_INTERVIEW_PERSON_SUCCESS = - "Interview of person named %1$s has been scheduled on %2$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in HR+."; + private final Logger logger = LogsCenter.getLogger(this.getClass()); - private final Index index; - private final LocalDateTime dateTime; + // For animation + private Person currentSelectedPerson; + private RadarChart radarChart; + private ArrayList allAnimation = new ArrayList<>(); + private LinkedList nodes = new LinkedList<>(); + private boolean animated; - private Person personToInterview; - private Person scheduledPerson; + @FXML + private AnchorPane infoPaneWrapper; + @FXML + private SplitPane infoSplitPane; - /** - * @param index of the person in the filtered person list to schedule interview - * @param dateTime of the interview - */ - public InterviewCommand(Index index, LocalDateTime dateTime) { - requireNonNull(index); - requireNonNull(dateTime); + // Responsive + @FXML + private ScrollPane infoMainPane; + @FXML + private ScrollPane infoSplitMainPane; + @FXML + private VBox infoMain; + @FXML + private AnchorPane infoMainRatings; + @FXML + private AnchorPane infoSplitSidePane; + @FXML + private VBox infoSplitRatings; - this.index = index; - this.dateTime = dateTime; - } + @FXML + private Label infoMainName; + @FXML + private Label infoMainUniversity; + @FXML + private Label infoMainMajorYear; + @FXML + private Label infoMainCgpa; + @FXML + private Label infoMainEmail; + @FXML + private Label infoMainAddress; + @FXML + private Label infoMainPhone; + @FXML + private Label infoMainPosition; + @FXML + private Label infoMainStatus; + @FXML + private Label infoMainComments; - public Index getIndex() { - return index; - } + // Animation + @FXML + private VBox infoMainPart; + @FXML + private HBox infoMainTopRight; + @FXML + private HBox infoMainContactEmailPane; + @FXML + private HBox infoMainContactAddressPane; + @FXML + private HBox infoMainContactPhonePane; + @FXML + private Label infoMainPositionLabel; + @FXML + private Label infoMainStatusLabel; + @FXML + private VBox infoMainCommentsPane; - public LocalDateTime getDateTime() { - return dateTime; - } + // Interview + @FXML + private VBox infoMainInterviewDatePane; + @FXML + private Label infoMainInterviewMonth; + @FXML + private Label infoMainInterviewDate; + @FXML + private Label infoMainInterviewDay; + @FXML + private Label infoMainInterviewTime; - public Person getPersonToInterview() { - return personToInterview; - } + // Rating + @FXML + private AnchorPane infoSideGraph; + @FXML + private ProgressBar infoRatingTechnical; + @FXML + private ProgressBar infoRatingCommunication; + @FXML + private ProgressBar infoRatingProblemSolving; + @FXML + private ProgressBar infoRatingExperience; + @FXML + private ProgressBar infoRatingOverall; + @FXML + private Label infoRatingTechnicalValue; + @FXML + private Label infoRatingCommunicationValue; + @FXML + private Label infoRatingProblemSolvingValue; + @FXML + private Label infoRatingExperienceValue; + @FXML + private Label infoRatingOverallValue; - @Override - public CommandResult executeUndoableCommand() throws CommandException { - try { - model.updatePerson(personToInterview, scheduledPerson); - } catch (DuplicatePersonException dpe) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } catch (PersonNotFoundException pnfe) { - throw new AssertionError("The target person cannot be missing"); - } + // Resume + @FXML + private Button infoSideButtonResume; - return new CommandResult(String.format(MESSAGE_INTERVIEW_PERSON_SUCCESS, - scheduledPerson.getName(), UiUtil.formatDate(dateTime))); - } + public InfoPanel(boolean animated) { + super(FXML); + this.animated = animated; + setupNodes(); - @Override - protected void preprocessUndoableCommand() throws CommandException { - List lastShownList = model.getFilteredPersonList(); + radarChart = new RadarChart(Rating.MAXIMUM_SCORE, animated); + infoSideGraph.getChildren().add(radarChart.getRoot()); - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } + infoPaneWrapper.widthProperty().addListener((observable, oldValue, newValue) -> { + handleResize(oldValue.intValue(), newValue.intValue()); + }); + handleResponsive((int) infoPaneWrapper.getWidth()); + registerAsAnEventHandler(this); + } - personToInterview = lastShownList.get(index.getZeroBased()); - scheduledPerson = createScheduledPerson(personToInterview, dateTime); + @FXML + private void showResume() { + raise(new ShowPanelRequestEvent(PdfPanel.PANEL_NAME)); } /** - * Creates and returns a {@code Person} with the details of {@code personToInterview} - * with updated with {@code dateTime}. - */ - private static Person createScheduledPerson(Person personToInterview, LocalDateTime dateTime) { - requireAllNonNull(personToInterview, dateTime); - - return new Person(personToInterview.getName(), personToInterview.getPhone(), personToInterview.getEmail(), - personToInterview.getAddress(), personToInterview.getUniversity(), - personToInterview.getExpectedGraduationYear(), - personToInterview.getMajor(), personToInterview.getGradePointAverage(), - personToInterview.getJobApplied(), personToInterview.getRating(), - personToInterview.getResume(), personToInterview.getProfileImage(), personToInterview.getComment(), - new InterviewDate(dateTime), personToInterview.getStatus(), - personToInterview.getTags()); - } - - @Override - public String getParsedResult() { - return "Parsed date: " + UiUtil.formatDate(dateTime); - } + * Handle resize when width changed event occurred, then decide whether should trigger responsive handler or not + * @param oldValue of the width property + * @param newValue of the width property + */ + private void handleResize(int oldValue, int newValue) { + // Process only if there are differences + int smaller = Math.min(oldValue, newValue); + int larger = Math.max(oldValue, newValue); - @Override - public boolean equals(Object other) { - // Short circuit if same object - if (other == this) { - return true; + if (smaller <= SPLIT_MIN_WIDTH && larger >= SPLIT_MIN_WIDTH) { + handleResponsive(newValue); } + } - // instanceof handles nulls - if (!(other instanceof InterviewCommand)) { - return false; - } + /** + * Handle responsiveness by checking if window should split into two based on {@code SPLIT_MIN_WIDTH} + * @param width of {@code InfoPanel} + */ + private void handleResponsive(int width) { + if (width >= SPLIT_MIN_WIDTH) { + infoSplitPane.setVisible(true); + infoMainPane.setVisible(false); - // State check - InterviewCommand i = (InterviewCommand) other; - return getIndex().equals(i.getIndex()) - && getDateTime().equals(i.getDateTime()) - && Objects.equals(getPersonToInterview(), i.getPersonToInterview()); - } -} -``` -###### /java/seedu/address/logic/commands/ShowCommand.java -``` java -/** - * Shows a specific panel - */ -public class ShowCommand extends Command { + infoMainRatings.getChildren().remove(infoSplitRatings); + infoSplitSidePane.getChildren().remove(infoSplitRatings); + infoMainPane.setContent(null); - public static final String COMMAND_WORD = "show"; + infoSplitMainPane.setContent(infoMain); + infoSplitSidePane.getChildren().add(infoSplitRatings); - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Shows a specific panel. The panel can be either 'info' or 'resume'.\n" - + "Make sure person are selected before calling this command.\n" - + "When resume is requested, it will only shows when it is available.\n" - + "Parameters: PANEL (must be either 'info' or 'resume', case sensitive)\n" - + "Example: " + COMMAND_WORD + " info"; + } else { + infoMainPane.setVisible(true); + infoSplitPane.setVisible(false); - public static final String PANEL_INFO = "info"; - public static final String PANEL_RESUME = "resume"; + infoMainRatings.getChildren().remove(infoSplitRatings); + infoSplitSidePane.getChildren().remove(infoSplitRatings); + infoSplitMainPane.setContent(null); - private static final String MESSAGE_NOT_SELECTED = "A person must be selected before showing a panel."; - private static final String MESSAGE_RESUME_NA = "The selected person doesn't have a resume"; - private static final String MESSAGE_INVALID_PANEL = - "Invalid panel requested. Only 'info' and 'resume' are allowed."; - private static final String MESSAGE_SHOW_SUCCESS = "Showing the requested panel"; + infoMainPane.setContent(infoMain); + infoMainRatings.getChildren().add(infoSplitRatings); + } + } /** - * Enumeration of acceptable panel + * Update the info panel with animation + * @param fadeOutOnly If set, data will not be loaded and info panel will be faded out */ - public enum Panel { - INFO, RESUME - } + private void animateUpdateInfoPanel(boolean fadeOutOnly) { + // Stop previously started animation + allAnimation.forEach(Animation::pause); + allAnimation.clear(); - private final Panel panel; + if (animated) { + ArrayList allFadeIn = new ArrayList<>(); + double delay = 0; - public ShowCommand(Panel panel) { - requireNonNull(panel); - this.panel = panel; - } + for (Node node : nodes) { + delay += ANIMATION_DELAY_MS; - @Override - public CommandResult execute() throws CommandException { - Person selectedPerson = model.getSelectedPerson(); - if (selectedPerson == null) { - throw new CommandException(MESSAGE_NOT_SELECTED); + Animation fadeIn = UiUtil.fadeNode(node, true, 0, MAX_ANIMATION_TIME_MS, e -> { }); + fadeIn.setDelay(Duration.millis(delay)); - } else { - switch (panel) { - case INFO: - EventsCenter.getInstance().post(new ShowPanelRequestEvent(InfoPanel.PANEL_NAME)); - break; - case RESUME: - if (selectedPerson.getResume().value != null) { - EventsCenter.getInstance().post(new ShowPanelRequestEvent(PdfPanel.PANEL_NAME)); - } else { - throw new CommandException(MESSAGE_RESUME_NA); - } - break; - default: - throw new CommandException(MESSAGE_INVALID_PANEL); + allAnimation.add(fadeIn); + allFadeIn.add(fadeIn); } - return new CommandResult(MESSAGE_SHOW_SUCCESS); - } - } + Animation fadeOut = UiUtil.fadeNode(infoMainPart, false, MAX_ANIMATION_TIME_MS, e -> { + if (!fadeOutOnly) { + nodes.forEach(node -> node.setOpacity(0)); + infoMainPart.setOpacity(1); + updateInfoPanel(); + allFadeIn.forEach(Animation::play); + } + }); + + allAnimation.add(fadeOut); + fadeOut.play(); - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof ShowCommand // instanceof handles nulls - && this.panel.equals(((ShowCommand) other).panel)); // state check - } -} -``` -###### /java/seedu/address/logic/parser/EditCommandParser.java -``` java - /** - * Parses {@code Optional profileImage} into a {@code Optional} - * if {@code profileImage} is non-empty. - * If profile image is present and equals to empty string, it will be parsed into a - * {@code ProfileImage} containing null value. - */ - private Optional parseProfileImageForEdit(Optional profileImage) - throws IllegalValueException { - assert profileImage != null; - if (!profileImage.isPresent()) { - return Optional.empty(); - } - if (profileImage.get().equals("")) { - return Optional.of(new ProfileImage(null)); } else { - return ParserUtil.parseProfileImage(profileImage); + infoMainPart.setOpacity(fadeOutOnly ? 0 : 1); + nodes.forEach(node -> node.setOpacity(1)); + updateInfoPanel(); } } /** - * Parses {@code Optional comment} into a {@code Optional} if {@code comment} is non-empty. + * Animate the display of rating (Progress bar and label) + * @param rating data to be animated */ - private Optional parseCommentForEdit(Optional comment) throws IllegalValueException { - assert comment != null; - if (!comment.isPresent()) { - return Optional.empty(); - } - if (comment.get().equals("")) { - return Optional.of(new Comment(null)); - } else { - return ParserUtil.parseComment(comment); - } - } -``` -###### /java/seedu/address/logic/parser/InterviewCommandParser.java -``` java -/** - * Parses input arguments and creates a new InterviewCommand object - */ -public class InterviewCommandParser implements Parser { + private void animateRating(Rating rating) { + // Process Rating info + LinkedHashMap ratingData = new LinkedHashMap<>(); + ArrayList>> ratingHelper = new ArrayList<>(); - public static final String MESSAGE_DATETIME_PARSE_FAIL = "Failed to parse the date time from the string: %1$s"; - private static final com.joestelmach.natty.Parser parser = new com.joestelmach.natty.Parser(); + if (Rating.isValidScore(rating.technicalSkillsScore) + && Rating.isValidScore(rating.communicationSkillsScore) + && Rating.isValidScore(rating.problemSolvingSkillsScore) + && Rating.isValidScore(rating.experienceScore)) { - /** - * Parses the given {@code String} of arguments in the context of the InterviewCommand - * and returns an InterviewCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public InterviewCommand parse(String args) throws ParseException { - try { - // Parse the arguments - String[] arguments = args.trim().split("\\s+", 2); - if (arguments.length != 2) { - throw new IllegalValueException("Invalid command, expected 2 arguments"); - } + ratingHelper.add(new Pair<>(rating.technicalSkillsScore, + new Pair<>(infoRatingTechnical, infoRatingTechnicalValue))); + ratingHelper.add(new Pair<>(rating.communicationSkillsScore, + new Pair<>(infoRatingCommunication, infoRatingCommunicationValue))); + ratingHelper.add(new Pair<>(rating.problemSolvingSkillsScore, + new Pair<>(infoRatingProblemSolving, infoRatingProblemSolvingValue))); + ratingHelper.add(new Pair<>(rating.experienceScore, + new Pair<>(infoRatingExperience, infoRatingExperienceValue))); + ratingHelper.add(new Pair<>(rating.overallScore, + new Pair<>(infoRatingOverall, infoRatingOverallValue))); - // Parse the index - Index index = ParserUtil.parseIndex(arguments[0]); + ratingData.put("Technical", rating.technicalSkillsScore); + ratingData.put("Communication", rating.communicationSkillsScore); + ratingData.put("Problem\nSolving", rating.problemSolvingSkillsScore); + ratingData.put("Experience", rating.experienceScore); - // Parse the date time - LocalDateTime dateTime = parseDateFromNaturalLanguage(arguments[1]); + for (Pair> entry : ratingHelper) { + double rateValue = entry.getKey() / Rating.MAXIMUM_SCORE; + ProgressBar progressBar = entry.getValue().getKey(); + Label label = entry.getValue().getValue(); - return new InterviewCommand(index, dateTime); + if (animated) { + DoubleProperty value = new SimpleDoubleProperty(0); - } catch (ParseException pe) { - throw pe; + Timeline timeline = new Timeline( + new KeyFrame(Duration.millis(rateValue * RadarChart.MAX_ANIMATION_TIME_MS), + new KeyValue(value, rateValue, UiUtil.EASE_OUT_CUBIC)) + ); + timeline.setAutoReverse(false); + timeline.setCycleCount(1); + timeline.play(); - } catch (IllegalValueException ive) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); - } - } + value.addListener((observable, oldValue, newValue) -> { + progressBar.setProgress(newValue.doubleValue()); + label.setText(UiUtil.toFixed(newValue.doubleValue() * Rating.MAXIMUM_SCORE, 2)); + }); - /** - * Parses the given natural language {@code String} and returns a {@code LocalDateTime} object - * that represents the English representation of the date and time - * @throws ParseException if the phrase cannot be converted to date and time - */ - private LocalDateTime parseDateFromNaturalLanguage(String naturalLanguage) throws ParseException { - List groups = parser.parse(naturalLanguage); - if (groups.size() < 1) { - throw new ParseException(String.format(MESSAGE_DATETIME_PARSE_FAIL, naturalLanguage)); - } + allAnimation.add(timeline); + } else { + progressBar.setProgress(rateValue); + label.setText(UiUtil.toFixed(rateValue * Rating.MAXIMUM_SCORE, 2)); + } + } - List dates = groups.get(0).getDates(); - if (dates.size() < 1) { - throw new ParseException(String.format(MESSAGE_DATETIME_PARSE_FAIL, naturalLanguage)); + } else { + infoRatingTechnical.setProgress(0); + infoRatingCommunication.setProgress(0); + infoRatingProblemSolving.setProgress(0); + infoRatingExperience.setProgress(0); + infoRatingOverall.setProgress(0); + + infoRatingTechnicalValue.setText("0.00"); + infoRatingCommunicationValue.setText("0.00"); + infoRatingProblemSolvingValue.setText("0.00"); + infoRatingExperienceValue.setText("0.00"); + infoRatingOverallValue.setText("0.00"); + + ratingData.put("Technical", 0.0); + ratingData.put("Communication", 0.0); + ratingData.put("Problem\nSolving", 0.0); + ratingData.put("Experience", 0.0); } - Date date = dates.get(0); - return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + radarChart.setData(ratingData); } -} -``` -###### /java/seedu/address/logic/parser/ParserUtil.java -``` java + /** - * Parses a {@code String profileImage} into a {@code ProfileImage}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws IllegalValueException if the given {@code profileImage} is invalid. + * Setup nodes to be animated, order matters */ - public static ProfileImage parseProfileImage(String profileImage) throws IllegalValueException { - requireNonNull(profileImage); - String trimmedProfileImage = profileImage.trim(); - if (!ProfileImage.isValidFile(trimmedProfileImage)) { - throw new IllegalValueException(ProfileImage.MESSAGE_IMAGE_CONSTRAINTS); - } - return new ProfileImage(trimmedProfileImage); + private void setupNodes() { + nodes.add(infoMainName); + nodes.add(infoMainTopRight); + nodes.add(infoMainUniversity); + nodes.add(infoMainMajorYear); + nodes.add(infoMainContactEmailPane); + nodes.add(infoMainContactAddressPane); + nodes.add(infoMainContactPhonePane); + nodes.add(infoMainInterviewDatePane); + nodes.add(infoMainPositionLabel); + nodes.add(infoMainPosition); + nodes.add(infoMainStatusLabel); + nodes.add(infoMainStatus); + nodes.add(infoMainCommentsPane); } /** - * Parses a {@code Optional profileImage} into an {@code Optional} - * if {@code profileImage} is present. - * See header comment of this class regarding the use of {@code Optional} parameters. + * Hide the info panel (When fade is done on MainWindow) */ - public static Optional parseProfileImage(Optional profileImage) throws IllegalValueException { - requireNonNull(profileImage); - return profileImage.isPresent() ? Optional.of(parseProfileImage(profileImage.get())) : Optional.empty(); + public void hide() { + infoMainPart.setOpacity(0); } /** - * Parses a {@code String comment} into a {@code Comment}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws IllegalValueException if the given {@code comment} is invalid. + * Show the info panel (When fade is done on MainWindow) */ - public static Comment parseComment(String comment) throws IllegalValueException { - requireNonNull(comment); - String trimmedComment = comment.trim(); - if (!Comment.isValidComment(trimmedComment)) { - throw new IllegalValueException(Comment.MESSAGE_COMMENT_CONSTRAINTS); - } - return new Comment(trimmedComment); + public void show() { + animateUpdateInfoPanel(false); } /** - * Parses a {@code Optional comment} into an {@code Optional} if {@code comment} is present. - * See header comment of this class regarding the use of {@code Comment} parameters. + * Update the info panel with the latest information + * It can be updated from address book change or selection change */ - public static Optional parseComment(Optional comment) throws IllegalValueException { - requireNonNull(comment); - return comment.isPresent() ? Optional.of(parseComment(comment.get())) : Optional.empty(); + private void updateInfoPanel() { + if (currentSelectedPerson == null) { + return; + } + + Person person = currentSelectedPerson; + + infoMainName.setText(person.getName().fullName); + infoMainUniversity.setText(person.getUniversity().value); + infoMainMajorYear.setText(person.getMajor() + " (Expected " + person.getExpectedGraduationYear().value + ")"); + infoMainCgpa.setText(UiUtil.toFixed(person.getGradePointAverage().value, 2)); + infoMainEmail.setText(person.getEmail().value); + infoMainAddress.setText(person.getAddress().value); + infoMainPhone.setText(person.getPhone().value); + infoMainPosition.setText(person.getJobApplied().value); + infoMainStatus.setText(person.getStatus().value); + infoMainStatus.setStyle("-fx-text-fill: " + UiUtil.colorToHex(person.getStatus().color)); + + // Update comment + String comment = person.getComment().value; + infoMainComments.setText(comment == null ? "" : comment); + + // Disable resume if it is null + boolean resumeAvailable = (person.getResume().value != null); + infoSideButtonResume.setDisable(!resumeAvailable); + infoSideButtonResume.setText(resumeAvailable ? "View resume" : "Resume not available"); + + // Process Interview info + LocalDateTime interviewDate = person.getInterviewDate().getDateTime(); + if (interviewDate != null) { + infoMainInterviewMonth.setText(interviewDate.getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH)); + infoMainInterviewDate.setText(String.valueOf(interviewDate.getDayOfMonth())); + infoMainInterviewDay.setText(interviewDate.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.ENGLISH)); + infoMainInterviewTime.setText(DateTimeFormatter.ofPattern("hh:mma", Locale.ENGLISH) + .format(interviewDate).toLowerCase()); + infoMainInterviewDatePane.setVisible(true); + } else { + infoMainInterviewDatePane.setVisible(false); + } + + animateRating(person.getRating()); + + // Scroll to top + infoMainPane.setVvalue(0); + infoSplitMainPane.setVvalue(0); + + // Set user data for test + infoPaneWrapper.setUserData(currentSelectedPerson); + infoPaneWrapper.fireEvent(new InfoPanelChangedEvent()); } -} -``` -###### /java/seedu/address/logic/parser/ShowCommandParser.java -``` java -/** - * Parses input arguments and creates a new ShowCommand object - */ -public class ShowCommandParser { - /** - * Parses the given {@code String} of arguments in the context of the ShowCommand - * and returns an ShowCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public ShowCommand parse(String args) throws ParseException { - requireNonNull(args); + @Subscribe + private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + PersonCard newSelected = event.getNewSelection(); + Person newSelectedPerson = (newSelected == null) ? null : newSelected.getPerson(); - // Parse the arguments - String requestedString = args.trim(); - ShowCommand.Panel requestedPanel = parsePanel(requestedString); - return new ShowCommand(requestedPanel); + if (newSelectedPerson == null) { + raise(new ShowPanelRequestEvent("WelcomePane")); + } else { + raise(new ShowPanelRequestEvent(InfoPanel.PANEL_NAME)); + } + + // Update if only the person selected is not what is currently shown + if (!Objects.equals(newSelectedPerson, currentSelectedPerson)) { + currentSelectedPerson = newSelectedPerson; + animateUpdateInfoPanel(currentSelectedPerson == null); + } } - /** - * Parses {@code panel} into a {@code ShowCommand.Panel} and returns it. - * Leading and trailing whitespaces will be trimmed. - * @throws ParseException if the specified panel is invalid (not info or resume). - */ - private ShowCommand.Panel parsePanel(String panel) throws ParseException { - String trimmed = panel.trim(); + @Subscribe + private void handlePersonChangedEvent(PersonChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + Person source = event.getSource(); + Person target = event.getTarget(); - switch (trimmed) { - case ShowCommand.PANEL_INFO: - return ShowCommand.Panel.INFO; - case ShowCommand.PANEL_RESUME: - return ShowCommand.Panel.RESUME; - default: - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ShowCommand.MESSAGE_USAGE)); + // Update if the person changed is what is currently shown + // Don't update if the person is the same (+ quickfix for Rating and Resume) + if (currentSelectedPerson != null && currentSelectedPerson.equals(source) && target != null + && !currentSelectedPerson.infoEquals(target)) { + currentSelectedPerson = target; + animateUpdateInfoPanel(false); } } } ``` -###### /java/seedu/address/model/person/Comment.java +###### /java/seedu/address/ui/PersonCard.java ``` java -/** - * Represents a Person's comment in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidComment(String)} - */ -public class Comment { + public PersonCard(Person person, int displayedIndex) { + super(FXML); - public static final String MESSAGE_COMMENT_CONSTRAINTS = "Person comment can take any values"; - public static final String COMMENT_VALIDATION_REGEX = ".*"; + this.person = person; + this.index = displayedIndex - 1; - public final String value; + cardPersonPane.setOpacity(0); + updatePersonCard(); - /** - * Constructs a {@code Comment}. - * - * @param comment A valid comment. - */ - public Comment(String comment) { - if (isNull(comment)) { - this.value = null; - } else { - checkArgument(isValidComment(comment), MESSAGE_COMMENT_CONSTRAINTS); - this.value = comment; - } + registerAsAnEventHandler(this); } /** - * Returns true if a given string is a valid comment. - * By default any string are valid + * Update the person with the latest information + * It can be updated from address book change or selection change */ - public static boolean isValidComment(String test) { - return test.matches(COMMENT_VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } + private void updatePersonCard() { + if (person == null) { + return; + } - @Override - public boolean equals(Object other) { - return other == this // Short circuit if same object - || (other instanceof Comment // instanceof handles nulls - && Objects.equals(this.value, ((Comment) other).value)); // State check - } + cardPhoto.fitWidthProperty().bind(cardPhotoMask.widthProperty()); + cardPhoto.fitHeightProperty().bind(cardPhotoMask.heightProperty()); - @Override - public int hashCode() { - return value.hashCode(); - } - -} -``` -###### /java/seedu/address/model/person/InterviewDate.java -``` java -/** - * Represents a Person's interview date in the address book. - * Guarantees: immutable - */ -public class InterviewDate { - public static final String MESSAGE_INTERVIEW_DATE_XML_ERROR = - "Interview date must be in epoch format, failed to parse from XML"; - public static final ZoneOffset LOCAL_ZONE_OFFSET = ZoneId.systemDefault().getRules().getOffset(LocalDateTime.now()); + Image profileImage = person.getProfileImage().getImage(); + if (profileImage == null) { + cardPhoto.setImage(null); + } else { + try { + cardPhoto.setImage(profileImage); + } catch (Exception e) { + logger.info("Failed to load image file"); + } + } - public final LocalDateTime dateTime; - public final String value; + cardPersonName.setText(person.getName().fullName); + cardPersonUniversity.setText(person.getUniversity().value); + cardPersonEmail.setText(person.getEmail().value); + cardPersonContact.setText(person.getPhone().value); + cardPersonStatus.setText(person.getStatus().value); + cardPersonStatus.setStyle("-fx-background-color: " + UiUtil.colorToHex(person.getStatus().color)); + cardPersonNumber.setText(String.valueOf(index + 1)); - /** - * Constructs a {@code InterviewDate}. - */ - public InterviewDate() { - this((LocalDateTime) null); + double rating = person.getRating().overallScore; + if (rating < 1e-3) { + cardPersonRating.setText(""); + iconRating.setVisible(false); + } else { + cardPersonRating.setText(UiUtil.toFixed(rating, 2)); + iconRating.setVisible(true); + } } /** - * Constructs a {@code InterviewDate}. - * @param timestamp A epoch timestamp + * Play animation (Fade is done on MainWindow) */ - public InterviewDate(Long timestamp) { - this(LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.UTC)); + public void play() { + Animation fadeIn = UiUtil.fadeNode(cardPersonPane, true, MAX_ANIMATION_TIME_MS, 0, e -> {}); + fadeIn.setDelay(Duration.millis(index * 50)); + fadeIn.play(); } /** - * Constructs a {@code InterviewDate}. - * @param dateTime of the person + * Show without animation (Fade is done on MainWindow) */ - public InterviewDate(LocalDateTime dateTime) { - this.dateTime = dateTime; - if (dateTime != null) { - this.value = String.valueOf(dateTime.toEpochSecond(ZoneOffset.UTC)); - } else { - this.value = null; - } - } - - public LocalDateTime getDateTime() { - return dateTime; + public void show() { + cardPersonPane.setOpacity(1); } - @Override - public String toString() { - return value; - } + @Subscribe + private void handlePersonChangedEvent(PersonChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } + Person source = event.getSource(); + Person target = event.getTarget(); - if (!(other instanceof InterviewDate)) { - return false; + if (person != null && person.equals(source)) { + person = target; + updatePersonCard(); } - - InterviewDate i = (InterviewDate) other; - return Objects.equals(getDateTime(), i.getDateTime()); } - - @Override - public int hashCode() { - return getDateTime().hashCode(); - } -} ``` -###### /java/seedu/address/model/person/ProfileImage.java +###### /java/seedu/address/ui/MainWindow.java ``` java -/** - * Represents a Person's profile image in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidFile(String)} - */ -public class ProfileImage { - public static final String MESSAGE_IMAGE_CONSTRAINTS = - "Profile image file should be at least 1 character long, exist in the same directory " - + "as the jar executable, smaller than 1MB and readable"; + public void requestFocus() { + primaryStage.requestFocus(); + } - private static final int ONEMEGABYTE = 1 * 1024 * 1024; - private static final String IMAGE_VALIDATION_REGEX = ".*\\S.*"; - private static final int CROP_DIMENSION = 100; + /** + * Handle responsiveness by fixing the width of {@code bottomListPane} + * when increasing the width of {@code bottomPaneSplit} + */ + private void handleSplitPaneResponsive() { + int splitHandleSize = 5; - public final String value; - public final String userInput; - private boolean isHashed; - private final Image image; + bottomPaneSplit.widthProperty().addListener((observable, oldValue, newValue) -> { + if (bottomInfoPane.getWidth() > bottomInfoPane.getMinWidth() - splitHandleSize) { + bottomPaneSplit.setDividerPosition(0, ( + bottomListPane.getWidth() + splitHandleSize) / newValue.doubleValue()); + } + }); + } /** - * Constructs a {@code ProfileImage}. + * Sets the accelerator of help button pane. * - * @param fileName A valid fileName. + * @param keyCombination the KeyCombination value of the accelerator */ - public ProfileImage(String fileName) { - isHashed = false; - if (isNull(fileName)) { - this.value = null; - this.userInput = null; - this.image = null; - } else { - checkArgument(isValidFile(fileName), MESSAGE_IMAGE_CONSTRAINTS); - this.value = fileName; - this.userInput = fileName; - this.image = loadImage(); - } + private void setAccelerator(Pane pane, KeyCombination keyCombination) { + primaryStage.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getTarget() instanceof TextInputControl && keyCombination.match(event)) { + pane.getOnMouseClicked().handle(new javafx.scene.input.MouseEvent( + javafx.scene.input.MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0, + MouseButton.PRIMARY, 0, false, false, false, + false, false, false, false, + false, false, false, null)); + event.consume(); + } + }); } - public ProfileImage(String storageFileName, String userFileName) { - isHashed = true; - if (isNull(storageFileName)) { - this.value = null; - this.userInput = null; - this.image = null; - } else { - checkArgument(isValidFile(storageFileName), MESSAGE_IMAGE_CONSTRAINTS); - this.value = storageFileName; - this.userInput = userFileName; - this.image = loadImage(); - } + private void setBorderlessWindow() { + // StageStyle.UNDECORATED is buggy + primaryStage.initStyle(StageStyle.TRANSPARENT); } - public Image getImage() { - return image; + private void setDoubleClickMaximize() { + topPane.setOnMouseClicked(event -> { + if (MouseButton.PRIMARY.equals(event.getButton()) && event.getClickCount() == 2) { + raise(new MaximizeAppRequestEvent()); + } + }); } - /** - * Return the loaded {@code Image} of the person's Profile Image, - * resized to 100px for performance issue - * @return the image in {@code Image} - */ - private Image loadImage() { - try { - File file = getFile(); - if (file != null) { - //Image image = new Image(file.toURI().toString(), 0, 0, true, true, true); + private void setDraggableTitleBar() { + double minY = Screen.getPrimary().getVisualBounds().getMinY(); - // Load image - BufferedImage image = ImageIO.read(file); + topPane.setOnMousePressed(event -> { + xOffset = event.getSceneX(); + yOffset = event.getSceneY(); + }); - // Scaling amd resizing calculation - int width = image.getWidth(); - int height = image.getHeight(); - int shorter = Math.min(width, height); - double scale = (double) shorter / (double) CROP_DIMENSION; - int x = 0; - int y = 0; + topPane.setOnMouseDragged(event -> { + // Only allow in title bar (Blue area) + if (xOffset > 120 && yOffset > 40 && xOffset + yOffset - 200 > 0) { + return; + } - if (width < height) { - width = CROP_DIMENSION; - height = (int) Math.round((double) height / scale); - y = (CROP_DIMENSION - height) / 2; - } else { - height = CROP_DIMENSION; - width = (int) Math.round((double) width / scale); - x = (CROP_DIMENSION - width) / 2; - } + double newY = event.getScreenY() - yOffset; + primaryStage.setX(event.getScreenX() - xOffset); + primaryStage.setY(Math.max(newY, minY)); + }); + } - // Resize start - BufferedImage resized = new BufferedImage(CROP_DIMENSION, CROP_DIMENSION, - BufferedImage.TYPE_4BYTE_ABGR); - Graphics2D g2d = resized.createGraphics(); - g2d.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_SPEED)); - g2d.drawImage(image, x, y, width, height, null); + @Subscribe + private void handleShowPanelRequestEvent(ShowPanelRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); - // Output - WritableImage output = new WritableImage(CROP_DIMENSION, CROP_DIMENSION); - SwingFXUtils.toFXImage(resized, output); + String requested = event.getRequestedPanel(); + Node toHide = activeNode; + Animation fadeIn; + Animation fadeOut; - // Clean up - image.flush(); - resized.flush(); - g2d.dispose(); + // Don't animate if the currently active panel is what requested + if (!Objects.equals(activePanel, requested)) { + // Pause all current running animation + allAnimation.forEach(Animation::pause); + allAnimation.clear(); - return output; - } - } catch (Exception e) { - return null; - } - return null; - } + activePanel = requested; - /** - * Return the {@code File} of the image - * @return the image in {@code File} - */ - private File getFile() { - if (this.value == null) { - return null; - } - return getFileFromPath(this.value); - } + // Show relevant panel + if (PdfPanel.PANEL_NAME.equals(requested)) { + pdfPanel.load(); + activeNode = resumePanePlaceholder; + } else if (InfoPanel.PANEL_NAME.equals(requested)) { + infoPanel.hide(); + activeNode = infoPanePlaceholder; + infoPanel.show(); + } else if ("WelcomePane".equals(requested)) { + activeNode = welcomePane; + } - /** - * Return the {@code File} representation of the path - * @param path of the image - * @return the {@code File} representation - */ - private static File getFileFromPath(String path) { - String userDir = System.getProperty("user.dir"); - return new File(userDir + File.separator + path); - } + if (activeNode == null) { + return; + } - /** - * Returns true if a given string is a valid file path, - * however it doesn't validate if it is a valid image file - * due to there are too many different image types - */ - public static boolean isValidFile(String test) { - requireNonNull(test); + if (animated) { + // Show currently requested panel + activeNode.setOpacity(0); + activeNode.setVisible(true); - if (!test.matches(IMAGE_VALIDATION_REGEX)) { - return false; - } + fadeIn = UiUtil.fadeNode(activeNode, true, MAX_ANIMATION_TIME_MS, ev -> { }); + allAnimation.add(fadeIn); + fadeIn.play(); - File imageFile = getFileFromPath(test); + // Hide the previously selected panel + if (toHide != null) { + fadeOut = UiUtil.fadeNode(toHide, false, + MAX_ANIMATION_TIME_MS, ev -> onFinishAnimation(toHide)); + allAnimation.add(fadeOut); + fadeOut.play(); + } + } else { + // Show currently requested panel + activeNode.setOpacity(1); + activeNode.setVisible(true); - if (imageFile.isDirectory() || !imageFile.exists() || imageFile.length() > ONEMEGABYTE) { - return false; - } else { - return true; + // Hide the previously selected panel + onFinishAnimation(toHide); + } } } - public boolean isHashed() { - return isHashed; - } - - @Override - public String toString() { - return userInput; - } - - @Override - public boolean equals(Object other) { - return other == this // Short circuit if same object - || (other instanceof ProfileImage // instanceof handles nulls - && ((this.value == null && ((ProfileImage) other).value == null) //both value are null - || (isHashed && ((ProfileImage) other).isHashed) ? isHashEqual(this.value, ((ProfileImage) other).value) - : this.userInput.equals(((ProfileImage) other).userInput))); // state check - } /** - * Checks whether the hash of two resume are the same - * @param first resume - * @param second resume - * @return same as true or false otherwise + * Hide and unload relevant nodes when animation is done playing + * @param toHide The window to hide */ - private boolean isHashEqual(String first, String second) { - assert(first.split("_").length == 2); - String firstHash = first.split("_")[1]; - String secondHash = second.split("_")[1]; - return firstHash.equals(secondHash); + private void onFinishAnimation(Node toHide) { + // Hide the previously selected panel + if (toHide != null) { + toHide.setVisible(false); + if (toHide.equals(resumePanePlaceholder)) { + pdfPanel.unload(); + } + } } - @Override - public int hashCode() { - return value.hashCode(); + @Subscribe + public void handleMinimizeAppRequestEvent(MinimizeAppRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + primaryStage.setIconified(true); } + @Subscribe + public void handleMaximizeAppRequestEvent(MaximizeAppRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + uiResizer.toggleMaximize(); + } } ``` -###### /java/seedu/address/ui/InfoPanel.java +###### /java/seedu/address/ui/PdfPanel.java ``` java /** - * The Info Panel of the App. + * The PDF Panel of the App */ -public class InfoPanel extends UiPart { +public class PdfPanel extends UiPart { - public static final Person DEFAULT_PERSON = null; - public static final int SPLIT_MIN_WIDTH = 550; + public static final String PANEL_NAME = "PdfPanel"; + private static final String FXML = "PdfPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(this.getClass()); - public static final String PANEL_NAME = "InfoPanel"; - private static final String FXML = "InfoPanel.fxml"; - private static final double MAX_ANIMATION_TIME_MS = 150; - private static final double ANIMATION_DELAY_MS = 15; + private PDDocument pdfDocument; + private ArrayList pdfPages; - private final Logger logger = LogsCenter.getLogger(this.getClass()); + private boolean forceUnload = false; + private boolean isLoaded = false; + private boolean loading = false; - // For animation - private Person currentSelectedPerson; - private RadarChart radarChart; - private ArrayList allAnimation = new ArrayList<>(); - private LinkedList nodes = new LinkedList<>(); - private boolean animated; + private Person selectedPerson = null; @FXML - private AnchorPane infoPaneWrapper; - @FXML - private SplitPane infoSplitPane; + private ScrollPane resumePane; - // Responsive - @FXML - private ScrollPane infoMainPane; - @FXML - private ScrollPane infoSplitMainPane; - @FXML - private VBox infoMain; - @FXML - private AnchorPane infoMainRatings; - @FXML - private AnchorPane infoSplitSidePane; @FXML - private VBox infoSplitRatings; + private VBox resumePanePages; @FXML - private Label infoMainName; - @FXML - private Label infoMainUniversity; - @FXML - private Label infoMainMajorYear; - @FXML - private Label infoMainCgpa; - @FXML - private Label infoMainEmail; - @FXML - private Label infoMainAddress; - @FXML - private Label infoMainPhone; - @FXML - private Label infoMainPosition; - @FXML - private Label infoMainStatus; - @FXML - private Label infoMainComments; + private Label resumePageLabel; - // Animation - @FXML - private VBox infoMainPart; @FXML - private HBox infoMainTopRight; - @FXML - private HBox infoMainContactEmailPane; - @FXML - private HBox infoMainContactAddressPane; - @FXML - private HBox infoMainContactPhonePane; - @FXML - private Label infoMainPositionLabel; - @FXML - private Label infoMainStatusLabel; - @FXML - private VBox infoMainCommentsPane; + private VBox resumeLoading; - // Interview - @FXML - private VBox infoMainInterviewDatePane; - @FXML - private Label infoMainInterviewMonth; - @FXML - private Label infoMainInterviewDate; - @FXML - private Label infoMainInterviewDay; @FXML - private Label infoMainInterviewTime; + private Label resumeLoadingLabel; - // Rating - @FXML - private AnchorPane infoSideGraph; - @FXML - private ProgressBar infoRatingTechnical; - @FXML - private ProgressBar infoRatingCommunication; @FXML - private ProgressBar infoRatingProblemSolving; - @FXML - private ProgressBar infoRatingExperience; - @FXML - private ProgressBar infoRatingOverall; - @FXML - private Label infoRatingTechnicalValue; - @FXML - private Label infoRatingCommunicationValue; - @FXML - private Label infoRatingProblemSolvingValue; - @FXML - private Label infoRatingExperienceValue; - @FXML - private Label infoRatingOverallValue; + private ProgressBar resumeLoadingBar; - // Resume - @FXML - private Button infoSideButtonResume; - public InfoPanel(boolean animated) { + + public PdfPanel() { super(FXML); - this.animated = animated; - setupNodes(); + setupEscKey(); - radarChart = new RadarChart(Rating.MAXIMUM_SCORE, animated); - infoSideGraph.getChildren().add(radarChart.getRoot()); + resumePane.widthProperty().addListener((observable, oldValue, newValue) -> handleResizeEvent()); + resumePane.vvalueProperty().addListener((observable, oldValue, newValue) -> handleScrollEvent()); - infoPaneWrapper.widthProperty().addListener((observable, oldValue, newValue) -> { - handleResize(oldValue.intValue(), newValue.intValue()); - }); - handleResponsive((int) infoPaneWrapper.getWidth()); registerAsAnEventHandler(this); } - @FXML - private void showResume() { - raise(new ShowPanelRequestEvent(PdfPanel.PANEL_NAME)); - } - /** - * Handle resize when width changed event occurred, then decide whether should trigger responsive handler or not - * @param oldValue of the width property - * @param newValue of the width property + * Setup binding for escape key to hide PDF panel */ - private void handleResize(int oldValue, int newValue) { - // Process only if there are differences - int smaller = Math.min(oldValue, newValue); - int larger = Math.max(oldValue, newValue); + private void setupEscKey() { + resumePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getCode().equals(KeyCode.ESCAPE)) { + cancelResume(); + event.consume(); + } + }); + } - if (smaller <= SPLIT_MIN_WIDTH && larger >= SPLIT_MIN_WIDTH) { - handleResponsive(newValue); - } + @FXML + private void cancelResume() { + raise(new ShowPanelRequestEvent(InfoPanel.PANEL_NAME)); } /** - * Handle responsiveness by checking if window should split into two based on {@code SPLIT_MIN_WIDTH} - * @param width of {@code InfoPanel} + * Load the resume of currently selected person */ - private void handleResponsive(int width) { - if (width >= SPLIT_MIN_WIDTH) { - infoSplitPane.setVisible(true); - infoMainPane.setVisible(false); + public void load() { + // Retry later if loading + if (loading) { + Platform.runLater(this::load); + return; + } - infoMainRatings.getChildren().remove(infoSplitRatings); - infoSplitSidePane.getChildren().remove(infoSplitRatings); - infoMainPane.setContent(null); + // Unload to make sure it is in a clean state + unload(); + if (selectedPerson == null) { + return; + } - infoSplitMainPane.setContent(infoMain); - infoSplitSidePane.getChildren().add(infoSplitRatings); + // Check if the user have resume + String filePath = selectedPerson.getResume().value; + if (filePath == null) { + return; + } - } else { - infoMainPane.setVisible(true); - infoSplitPane.setVisible(false); + // Start loading + loading = true; - infoMainRatings.getChildren().remove(infoSplitRatings); - infoSplitSidePane.getChildren().remove(infoSplitRatings); - infoSplitMainPane.setContent(null); + // Show progress to user + resumePane.setVisible(false); + resumeLoadingLabel.setText("Opening " + selectedPerson.getName().fullName + "'s resume"); + resumeLoadingBar.setProgress(0); + resumeLoading.setVisible(true); - infoMainPane.setContent(infoMain); - infoMainRatings.getChildren().add(infoSplitRatings); - } + // Start loading in new thread + Thread pdfThread = new Thread(() -> loadInNewThread(filePath)); + pdfThread.start(); } /** - * Update the info panel with animation - * @param fadeOutOnly If set, data will not be loaded and info panel will be faded out + * Load resume and render into images in a new thread to prevent thread blocking + * @param filePath of the resume */ - private void animateUpdateInfoPanel(boolean fadeOutOnly) { - // Stop previously started animation - allAnimation.forEach(Animation::pause); - allAnimation.clear(); - - if (animated) { - ArrayList allFadeIn = new ArrayList<>(); - double delay = 0; + private void loadInNewThread(String filePath) { + ArrayList pages = new ArrayList<>(); - for (Node node : nodes) { - delay += ANIMATION_DELAY_MS; + try { + // PDF renderer + PDDocument pdfDocument = PDDocument.load(new File(filePath)); + PDFRenderer pdfRenderer = new PDFRenderer(pdfDocument); + BufferedImage bufferedImage; - Animation fadeIn = UiUtil.fadeNode(node, true, 0, MAX_ANIMATION_TIME_MS, e -> { }); - fadeIn.setDelay(Duration.millis(delay)); + int totalPages = pdfDocument.getNumberOfPages(); - allAnimation.add(fadeIn); - allFadeIn.add(fadeIn); - } + // Generate all images + for (int currentPage = 0; currentPage < totalPages; currentPage++) { + try { + bufferedImage = pdfRenderer.renderImageWithDPI(currentPage, 150, ImageType.RGB); + pages.add(SwingFXUtils.toFXImage(bufferedImage, null)); + } catch (IOException e) { + logger.info("PdfPanel: Page " + currentPage + " render failed"); + pages.add(null); + } - Animation fadeOut = UiUtil.fadeNode(infoMainPart, false, MAX_ANIMATION_TIME_MS, e -> { - if (!fadeOutOnly) { - nodes.forEach(node -> node.setOpacity(0)); - infoMainPart.setOpacity(1); - updateInfoPanel(); - allFadeIn.forEach(Animation::play); + // Stop thread and pass back if force unload + if (forceUnload) { + Platform.runLater(() -> imageLoaded(null, null)); + return; } - }); - allAnimation.add(fadeOut); - fadeOut.play(); + // Update on main thread + int currentLoaded = currentPage + 1; + Platform.runLater(() -> update(currentLoaded, totalPages)); + } - } else { - infoMainPart.setOpacity(fadeOutOnly ? 0 : 1); - nodes.forEach(node -> node.setOpacity(1)); - updateInfoPanel(); + // Pass back to main thread + Platform.runLater(() -> imageLoaded(pdfDocument, pages)); + + } catch (IOException e) { + logger.info("PdfPanel: Load of file " + filePath + " failed"); + // Pass back to main thread + Platform.runLater(() -> imageLoaded(null, null)); } } /** - * Animate the display of rating (Progress bar and label) - * @param rating data to be animated + * Update status of the rendering so user know the status + * @param currentLoaded number of pages loaded + * @param totalPages of the PDF document */ - private void animateRating(Rating rating) { - // Process Rating info - LinkedHashMap ratingData = new LinkedHashMap<>(); - ArrayList>> ratingHelper = new ArrayList<>(); - - if (Rating.isValidScore(rating.technicalSkillsScore) - && Rating.isValidScore(rating.communicationSkillsScore) - && Rating.isValidScore(rating.problemSolvingSkillsScore) - && Rating.isValidScore(rating.experienceScore)) { - - ratingHelper.add(new Pair<>(rating.technicalSkillsScore, - new Pair<>(infoRatingTechnical, infoRatingTechnicalValue))); - ratingHelper.add(new Pair<>(rating.communicationSkillsScore, - new Pair<>(infoRatingCommunication, infoRatingCommunicationValue))); - ratingHelper.add(new Pair<>(rating.problemSolvingSkillsScore, - new Pair<>(infoRatingProblemSolving, infoRatingProblemSolvingValue))); - ratingHelper.add(new Pair<>(rating.experienceScore, - new Pair<>(infoRatingExperience, infoRatingExperienceValue))); - ratingHelper.add(new Pair<>(rating.overallScore, - new Pair<>(infoRatingOverall, infoRatingOverallValue))); + private void update(int currentLoaded, int totalPages) { + resumeLoadingLabel.setText("Loading page " + currentLoaded + " of " + totalPages); + resumeLoadingBar.setProgress((double) currentLoaded / (double) totalPages); + } - ratingData.put("Technical", rating.technicalSkillsScore); - ratingData.put("Communication", rating.communicationSkillsScore); - ratingData.put("Problem\nSolving", rating.problemSolvingSkillsScore); - ratingData.put("Experience", rating.experienceScore); + /** + * Callback from separate thread indicates all pages are loaded and rendered + * @param pdfDocument of the opened document + * @param pages An array of images of all the pages + */ + private void imageLoaded(PDDocument pdfDocument, ArrayList pages) { + this.pdfDocument = pdfDocument; + pdfPages = pages; - for (Pair> entry : ratingHelper) { - double rateValue = entry.getKey() / Rating.MAXIMUM_SCORE; - ProgressBar progressBar = entry.getValue().getKey(); - Label label = entry.getValue().getValue(); + if (pages == null) { + isLoaded = false; - if (animated) { - DoubleProperty value = new SimpleDoubleProperty(0); + } else { + int totalPages = pdfPages.size(); - Timeline timeline = new Timeline( - new KeyFrame(Duration.millis(rateValue * RadarChart.MAX_ANIMATION_TIME_MS), - new KeyValue(value, rateValue, UiUtil.EASE_OUT_CUBIC)) - ); - timeline.setAutoReverse(false); - timeline.setCycleCount(1); - timeline.play(); + // Setup all blank pages + for (int i = 0; i < totalPages; i++) { + // Wrap inside VBox for styling + VBox vBox = new VBox(); + vBox.getStyleClass().add("pdf-page"); + VBox.setMargin(vBox, new Insets(0, 0, 20, 0)); - value.addListener((observable, oldValue, newValue) -> { - progressBar.setProgress(newValue.doubleValue()); - label.setText(UiUtil.toFixed(newValue.doubleValue() * Rating.MAXIMUM_SCORE, 2)); - }); + // Setup VBox children (ImageView) + ImageView imageView = new ImageView(); + imageView.setPreserveRatio(true); + imageView.setCache(true); - allAnimation.add(timeline); - } else { - progressBar.setProgress(rateValue); - label.setText(UiUtil.toFixed(rateValue * Rating.MAXIMUM_SCORE, 2)); - } + // Add into view + vBox.getChildren().add(imageView); + resumePanePages.getChildren().add(vBox); } - } else { - infoRatingTechnical.setProgress(0); - infoRatingCommunication.setProgress(0); - infoRatingProblemSolving.setProgress(0); - infoRatingExperience.setProgress(0); - infoRatingOverall.setProgress(0); + // Initialize size and scroll detection + handleResizeEvent(); + handleScrollEvent(); - infoRatingTechnicalValue.setText("0.00"); - infoRatingCommunicationValue.setText("0.00"); - infoRatingProblemSolvingValue.setText("0.00"); - infoRatingExperienceValue.setText("0.00"); - infoRatingOverallValue.setText("0.00"); + // Set label to first page + resumePageLabel.setText(1 + " / " + totalPages); + resumePageLabel.setVisible(true); - ratingData.put("Technical", 0.0); - ratingData.put("Communication", 0.0); - ratingData.put("Problem\nSolving", 0.0); - ratingData.put("Experience", 0.0); + isLoaded = true; } - radarChart.setData(ratingData); + resumePane.setVisible(true); + resumeLoading.setVisible(false); + loading = false; } /** - * Setup nodes to be animated, order matters + * Unload the PDFPanel to free up resources */ - private void setupNodes() { - nodes.add(infoMainName); - nodes.add(infoMainTopRight); - nodes.add(infoMainUniversity); - nodes.add(infoMainMajorYear); - nodes.add(infoMainContactEmailPane); - nodes.add(infoMainContactAddressPane); - nodes.add(infoMainContactPhonePane); - nodes.add(infoMainInterviewDatePane); - nodes.add(infoMainPositionLabel); - nodes.add(infoMainPosition); - nodes.add(infoMainStatusLabel); - nodes.add(infoMainStatus); - nodes.add(infoMainCommentsPane); - } + public void unload() { + // Force unload enabled + forceUnload = true; - /** - * Hide the info panel (When fade is done on MainWindow) - */ - public void hide() { - infoMainPart.setOpacity(0); + // Retry later if loading + if (loading) { + Platform.runLater(this::unload); + return; + } + + // Only unload when it is imageLoaded + if (isLoaded) { + isLoaded = false; + + // Clear all array + pdfPages.clear(); + resumePanePages.getChildren().clear(); + + // Hide page + resumePageLabel.setVisible(false); + resumePane.setVisible(false); + + try { + if (pdfDocument != null) { + pdfDocument.close(); + } + } catch (IOException e) { + logger.info("PdfPanel: Unload failed"); + } + } + + forceUnload = false; } /** - * Show the info panel (When fade is done on MainWindow) + * Handle the resize event by resizing all pages */ - public void show() { - animateUpdateInfoPanel(false); + private void handleResizeEvent() { + // Fit all images to width of the viewport + double width = resumePane.getWidth() - 40; + ObservableList childrens = resumePanePages.getChildren(); + + // Resize all the images + for (int i = 0; i < childrens.size(); i++) { + VBox vBox = (VBox) childrens.get(i); + ImageView imageView = (ImageView) vBox.getChildren().get(0); + Image page = pdfPages.get(i); + + // Size have to be fixed so it can maintain the size even when images outside of viewport are cleared + double aspectRatio = page.getWidth() / page.getHeight(); + imageView.setFitWidth(width); + imageView.setFitHeight(width / aspectRatio); + } } /** - * Update the info panel with the latest information - * It can be updated from address book change or selection change + * Handle the scroll event to lazy load the images into ImageView for performance */ - private void updateInfoPanel() { - if (currentSelectedPerson == null) { - return; - } - - Person person = currentSelectedPerson; - - infoMainName.setText(person.getName().fullName); - infoMainUniversity.setText(person.getUniversity().value); - infoMainMajorYear.setText(person.getMajor() + " (Expected " + person.getExpectedGraduationYear().value + ")"); - infoMainCgpa.setText(UiUtil.toFixed(person.getGradePointAverage().value, 2)); - infoMainEmail.setText(person.getEmail().value); - infoMainAddress.setText(person.getAddress().value); - infoMainPhone.setText(person.getPhone().value); - infoMainPosition.setText(person.getJobApplied().value); - infoMainStatus.setText(person.getStatus().value); - infoMainStatus.setStyle("-fx-text-fill: " + UiUtil.colorToHex(person.getStatus().color)); + private void handleScrollEvent() { + ObservableList childrens = resumePanePages.getChildren(); + int totalPages = childrens.size(); - // Update comment - String comment = person.getComment().value; - infoMainComments.setText(comment == null ? "" : comment); + // Compute view boundary (Only display image if visible) + Bounds viewBound = resumePane.localToScene(resumePane.getBoundsInLocal()); + double viewMinY = viewBound.getMinY(); + double viewMaxY = viewBound.getMaxY(); + double viewMid = (viewMinY + viewMaxY) / 2; - // Disable resume if it is null - boolean resumeAvailable = (person.getResume().value != null); - infoSideButtonResume.setDisable(!resumeAvailable); - infoSideButtonResume.setText(resumeAvailable ? "View resume" : "Resume not available"); + for (int i = 0; i < totalPages; i++) { - // Process Interview info - LocalDateTime interviewDate = person.getInterviewDate().getDateTime(); - if (interviewDate != null) { - infoMainInterviewMonth.setText(interviewDate.getMonth().getDisplayName(TextStyle.SHORT, Locale.ENGLISH)); - infoMainInterviewDate.setText(String.valueOf(interviewDate.getDayOfMonth())); - infoMainInterviewDay.setText(interviewDate.getDayOfWeek().getDisplayName(TextStyle.SHORT, Locale.ENGLISH)); - infoMainInterviewTime.setText(DateTimeFormatter.ofPattern("hh:mma", Locale.ENGLISH) - .format(interviewDate).toLowerCase()); - infoMainInterviewDatePane.setVisible(true); - } else { - infoMainInterviewDatePane.setVisible(false); - } + // Compute page boundary + VBox vBox = (VBox) childrens.get(i); + Bounds bounds = vBox.localToScene(vBox.getBoundsInLocal()); - animateRating(person.getRating()); + ImageView imageView = (ImageView) vBox.getChildren().get(0); - // Scroll to top - infoMainPane.setVvalue(0); - infoSplitMainPane.setVvalue(0); + // Check if page is visible in viewport + if (bounds.getMinY() < viewMaxY && bounds.getMaxY() > viewMinY) { + if (imageView.getImage() == null) { + imageView.setImage(pdfPages.get(i)); + } + } else { + imageView.setImage(null); + } - // Set user data for test - infoPaneWrapper.setUserData(currentSelectedPerson); - infoPaneWrapper.fireEvent(new InfoPanelChangedEvent()); + // Update page number label + if (bounds.getMinY() < viewMid && bounds.getMaxY() > viewMid) { + resumePageLabel.setText((i + 1) + " / " + totalPages); + } + } } @Subscribe private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); - PersonCard newSelected = event.getNewSelection(); - Person newSelectedPerson = (newSelected == null) ? null : newSelected.getPerson(); - - if (newSelectedPerson == null) { - raise(new ShowPanelRequestEvent("WelcomePane")); - } else { - raise(new ShowPanelRequestEvent(InfoPanel.PANEL_NAME)); - } - - // Update if only the person selected is not what is currently shown - if (!Objects.equals(newSelectedPerson, currentSelectedPerson)) { - currentSelectedPerson = newSelectedPerson; - animateUpdateInfoPanel(currentSelectedPerson == null); - } + PersonCard selectedCard = event.getNewSelection(); + selectedPerson = (selectedCard == null) ? null : selectedCard.getPerson(); } @Subscribe @@ -1319,1514 +1207,962 @@ public class InfoPanel extends UiPart { Person source = event.getSource(); Person target = event.getTarget(); - // Update if the person changed is what is currently shown - // Don't update if the person is the same (+ quickfix for Rating and Resume) - if (currentSelectedPerson != null && currentSelectedPerson.equals(source) && target != null - && !currentSelectedPerson.infoEquals(target)) { - currentSelectedPerson = target; - animateUpdateInfoPanel(false); + if (selectedPerson != null && selectedPerson.equals(source)) { + selectedPerson = target; } } } ``` -###### /java/seedu/address/ui/MainWindow.java +###### /java/seedu/address/ui/CommandBox.java ``` java - public void requestFocus() { - primaryStage.requestFocus(); - } + private void setupInputChange() { + commandInput.textProperty().addListener((obs, old, inputText) -> { + String result = "Enter a command..."; - /** - * Handle responsiveness by fixing the width of {@code bottomListPane} - * when increasing the width of {@code bottomPaneSplit} - */ - private void handleSplitPaneResponsive() { - int splitHandleSize = 5; + floatParseRealTime.getStyleClass().remove(PARSE_VALID); + floatParseRealTime.getStyleClass().remove(PARSE_INVALID); - bottomPaneSplit.widthProperty().addListener((observable, oldValue, newValue) -> { - if (bottomInfoPane.getWidth() > bottomInfoPane.getMinWidth() - splitHandleSize) { - bottomPaneSplit.setDividerPosition(0, ( - bottomListPane.getWidth() + splitHandleSize) / newValue.doubleValue()); + if (!inputText.equals("")) { + Command command = logic.parse(inputText); + + if (command == null) { + floatParseRealTime.getStyleClass().add(PARSE_INVALID); + result = "Invalid command"; + + } else { + floatParseRealTime.getStyleClass().add(PARSE_VALID); + result = command.getParsedResult(); + if (result == null) { + result = "Valid command"; + } + } + } + floatParseLabel.setText(result); + }); + + commandInput.focusedProperty().addListener((obs, old, focused) -> { + if (animated) { + Animation animation; + allAnimation.forEach(Animation::pause); + allAnimation.clear(); + + if (focused) { + floatParseRealTime.setOpacity(0); + floatParseRealTime.setVisible(true); + animation = UiUtil.fadeNode(floatParseRealTime, true, 100, (e) -> { + }); + } else { + animation = UiUtil.fadeNode(floatParseRealTime, false, 100, (e) -> { + floatParseRealTime.setVisible(false); + }); + } + + allAnimation.add(animation); + animation.play(); + } else { + floatParseRealTime.setOpacity(focused ? 1 : 0); + floatParseRealTime.setVisible(focused); } }); } +``` +###### /java/seedu/address/model/person/InterviewDate.java +``` java +/** + * Represents a Person's interview date in the address book. + * Guarantees: immutable + */ +public class InterviewDate { + public static final String MESSAGE_INTERVIEW_DATE_XML_ERROR = + "Interview date must be in epoch format, failed to parse from XML"; + public static final ZoneOffset LOCAL_ZONE_OFFSET = ZoneId.systemDefault().getRules().getOffset(LocalDateTime.now()); + + public final LocalDateTime dateTime; + public final String value; /** - * Sets the accelerator of help button pane. - * - * @param keyCombination the KeyCombination value of the accelerator + * Constructs a {@code InterviewDate}. */ - private void setAccelerator(Pane pane, KeyCombination keyCombination) { - primaryStage.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getTarget() instanceof TextInputControl && keyCombination.match(event)) { - pane.getOnMouseClicked().handle(new javafx.scene.input.MouseEvent( - javafx.scene.input.MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0, - MouseButton.PRIMARY, 0, false, false, false, - false, false, false, false, - false, false, false, null)); - event.consume(); - } - }); + public InterviewDate() { + this((LocalDateTime) null); } - private void setBorderlessWindow() { - // StageStyle.UNDECORATED is buggy - primaryStage.initStyle(StageStyle.TRANSPARENT); + /** + * Constructs a {@code InterviewDate}. + * @param timestamp A epoch timestamp + */ + public InterviewDate(Long timestamp) { + this(LocalDateTime.ofEpochSecond(timestamp, 0, ZoneOffset.UTC)); } - private void setDoubleClickMaximize() { - topPane.setOnMouseClicked(event -> { - if (MouseButton.PRIMARY.equals(event.getButton()) && event.getClickCount() == 2) { - raise(new MaximizeAppRequestEvent()); - } - }); + /** + * Constructs a {@code InterviewDate}. + * @param dateTime of the person + */ + public InterviewDate(LocalDateTime dateTime) { + this.dateTime = dateTime; + if (dateTime != null) { + this.value = String.valueOf(dateTime.toEpochSecond(ZoneOffset.UTC)); + } else { + this.value = null; + } } - private void setDraggableTitleBar() { - double minY = Screen.getPrimary().getVisualBounds().getMinY(); + public LocalDateTime getDateTime() { + return dateTime; + } - topPane.setOnMousePressed(event -> { - xOffset = event.getSceneX(); - yOffset = event.getSceneY(); - }); + @Override + public String toString() { + return value; + } - topPane.setOnMouseDragged(event -> { - // Only allow in title bar (Blue area) - if (xOffset > 120 && yOffset > 40 && xOffset + yOffset - 200 > 0) { - return; - } - - double newY = event.getScreenY() - yOffset; - primaryStage.setX(event.getScreenX() - xOffset); - primaryStage.setY(Math.max(newY, minY)); - }); - } - - @Subscribe - private void handleShowPanelRequestEvent(ShowPanelRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - - String requested = event.getRequestedPanel(); - Node toHide = activeNode; - Animation fadeIn; - Animation fadeOut; - - // Don't animate if the currently active panel is what requested - if (!Objects.equals(activePanel, requested)) { - // Pause all current running animation - allAnimation.forEach(Animation::pause); - allAnimation.clear(); - - activePanel = requested; - - // Show relevant panel - if (PdfPanel.PANEL_NAME.equals(requested)) { - pdfPanel.load(); - activeNode = resumePanePlaceholder; - } else if (InfoPanel.PANEL_NAME.equals(requested)) { - infoPanel.hide(); - activeNode = infoPanePlaceholder; - infoPanel.show(); - } else if ("WelcomePane".equals(requested)) { - activeNode = welcomePane; - } - - if (activeNode == null) { - return; - } - - if (animated) { - // Show currently requested panel - activeNode.setOpacity(0); - activeNode.setVisible(true); - - fadeIn = UiUtil.fadeNode(activeNode, true, MAX_ANIMATION_TIME_MS, ev -> { }); - allAnimation.add(fadeIn); - fadeIn.play(); - - // Hide the previously selected panel - if (toHide != null) { - fadeOut = UiUtil.fadeNode(toHide, false, - MAX_ANIMATION_TIME_MS, ev -> onFinishAnimation(toHide)); - allAnimation.add(fadeOut); - fadeOut.play(); - } - } else { - // Show currently requested panel - activeNode.setOpacity(1); - activeNode.setVisible(true); - - // Hide the previously selected panel - onFinishAnimation(toHide); - } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; } - } - /** - * Hide and unload relevant nodes when animation is done playing - * @param toHide The window to hide - */ - private void onFinishAnimation(Node toHide) { - // Hide the previously selected panel - if (toHide != null) { - toHide.setVisible(false); - if (toHide.equals(resumePanePlaceholder)) { - pdfPanel.unload(); - } + if (!(other instanceof InterviewDate)) { + return false; } - } - @Subscribe - public void handleMinimizeAppRequestEvent(MinimizeAppRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - primaryStage.setIconified(true); + InterviewDate i = (InterviewDate) other; + return Objects.equals(getDateTime(), i.getDateTime()); } - @Subscribe - public void handleMaximizeAppRequestEvent(MaximizeAppRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - uiResizer.toggleMaximize(); + @Override + public int hashCode() { + return getDateTime().hashCode(); } } ``` -###### /java/seedu/address/ui/PdfPanel.java +###### /java/seedu/address/model/person/ProfileImage.java ``` java /** - * The PDF Panel of the App + * Represents a Person's profile image in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidFile(String)} */ -public class PdfPanel extends UiPart { - - public static final String PANEL_NAME = "PdfPanel"; - private static final String FXML = "PdfPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(this.getClass()); - - private PDDocument pdfDocument; - private ArrayList pdfPages; +public class ProfileImage { + public static final String MESSAGE_IMAGE_CONSTRAINTS = + "Profile image file should be at least 1 character long, exist in the same directory " + + "as the jar executable, smaller than 1MB and readable"; - private boolean forceUnload = false; - private boolean isLoaded = false; - private boolean loading = false; + private static final int ONEMEGABYTE = 1 * 1024 * 1024; + private static final String IMAGE_VALIDATION_REGEX = ".*\\S.*"; + private static final int CROP_DIMENSION = 100; - private Person selectedPerson = null; + public final String value; + public final String userInput; + private boolean isHashed; + private final Image image; - @FXML - private ScrollPane resumePane; + /** + * Constructs a {@code ProfileImage}. + * + * @param fileName A valid fileName. + */ + public ProfileImage(String fileName) { + isHashed = false; + if (isNull(fileName)) { + this.value = null; + this.userInput = null; + this.image = null; + } else { + checkArgument(isValidFile(fileName), MESSAGE_IMAGE_CONSTRAINTS); + this.value = fileName; + this.userInput = fileName; + this.image = loadImage(); + } + } - @FXML - private VBox resumePanePages; + public ProfileImage(String storageFileName, String userFileName) { + isHashed = true; + if (isNull(storageFileName)) { + this.value = null; + this.userInput = null; + this.image = null; + } else { + checkArgument(isValidFile(storageFileName), MESSAGE_IMAGE_CONSTRAINTS); + this.value = storageFileName; + this.userInput = userFileName; + this.image = loadImage(); + } + } - @FXML - private Label resumePageLabel; + public Image getImage() { + return image; + } - @FXML - private VBox resumeLoading; + /** + * Return the loaded {@code Image} of the person's Profile Image, + * resized to 100px for performance issue + * @return the image in {@code Image} + */ + private Image loadImage() { + try { + File file = getFile(); + if (file != null) { + //Image image = new Image(file.toURI().toString(), 0, 0, true, true, true); - @FXML - private Label resumeLoadingLabel; + // Load image + BufferedImage image = ImageIO.read(file); - @FXML - private ProgressBar resumeLoadingBar; + // Scaling amd resizing calculation + int width = image.getWidth(); + int height = image.getHeight(); + int shorter = Math.min(width, height); + double scale = (double) shorter / (double) CROP_DIMENSION; + int x = 0; + int y = 0; + if (width < height) { + width = CROP_DIMENSION; + height = (int) Math.round((double) height / scale); + y = (CROP_DIMENSION - height) / 2; + } else { + height = CROP_DIMENSION; + width = (int) Math.round((double) width / scale); + x = (CROP_DIMENSION - width) / 2; + } + // Resize start + BufferedImage resized = new BufferedImage(CROP_DIMENSION, CROP_DIMENSION, + BufferedImage.TYPE_4BYTE_ABGR); + Graphics2D g2d = resized.createGraphics(); + g2d.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_SPEED)); + g2d.drawImage(image, x, y, width, height, null); - public PdfPanel() { - super(FXML); - setupEscKey(); + // Output + WritableImage output = new WritableImage(CROP_DIMENSION, CROP_DIMENSION); + SwingFXUtils.toFXImage(resized, output); - resumePane.widthProperty().addListener((observable, oldValue, newValue) -> handleResizeEvent()); - resumePane.vvalueProperty().addListener((observable, oldValue, newValue) -> handleScrollEvent()); + // Clean up + image.flush(); + resized.flush(); + g2d.dispose(); - registerAsAnEventHandler(this); + return output; + } + } catch (Exception e) { + return null; + } + return null; } /** - * Setup binding for escape key to hide PDF panel + * Return the {@code File} of the image + * @return the image in {@code File} */ - private void setupEscKey() { - resumePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getCode().equals(KeyCode.ESCAPE)) { - cancelResume(); - event.consume(); - } - }); + private File getFile() { + if (this.value == null) { + return null; + } + return getFileFromPath(this.value); } - @FXML - private void cancelResume() { - raise(new ShowPanelRequestEvent(InfoPanel.PANEL_NAME)); + /** + * Return the {@code File} representation of the path + * @param path of the image + * @return the {@code File} representation + */ + private static File getFileFromPath(String path) { + String userDir = System.getProperty("user.dir"); + return new File(userDir + File.separator + path); } /** - * Load the resume of currently selected person + * Returns true if a given string is a valid file path, + * however it doesn't validate if it is a valid image file + * due to there are too many different image types */ - public void load() { - // Retry later if loading - if (loading) { - Platform.runLater(this::load); - return; - } + public static boolean isValidFile(String test) { + requireNonNull(test); - // Unload to make sure it is in a clean state - unload(); - if (selectedPerson == null) { - return; + if (!test.matches(IMAGE_VALIDATION_REGEX)) { + return false; } - // Check if the user have resume - String filePath = selectedPerson.getResume().value; - if (filePath == null) { - return; - } + File imageFile = getFileFromPath(test); - // Start loading - loading = true; + if (imageFile.isDirectory() || !imageFile.exists() || imageFile.length() > ONEMEGABYTE) { + return false; + } else { + return true; + } + } - // Show progress to user - resumePane.setVisible(false); - resumeLoadingLabel.setText("Opening " + selectedPerson.getName().fullName + "'s resume"); - resumeLoadingBar.setProgress(0); - resumeLoading.setVisible(true); + public boolean isHashed() { + return isHashed; + } - // Start loading in new thread - Thread pdfThread = new Thread(() -> loadInNewThread(filePath)); - pdfThread.start(); + @Override + public String toString() { + return userInput; } + @Override + public boolean equals(Object other) { + return other == this // Short circuit if same object + || (other instanceof ProfileImage // instanceof handles nulls + && ((this.value == null && ((ProfileImage) other).value == null) //both value are null + || (isHashed && ((ProfileImage) other).isHashed) ? isHashEqual(this.value, ((ProfileImage) other).value) + : this.userInput.equals(((ProfileImage) other).userInput))); // state check + } /** - * Load resume and render into images in a new thread to prevent thread blocking - * @param filePath of the resume + * Checks whether the hash of two resume are the same + * @param first resume + * @param second resume + * @return same as true or false otherwise */ - private void loadInNewThread(String filePath) { - ArrayList pages = new ArrayList<>(); - - try { - // PDF renderer - PDDocument pdfDocument = PDDocument.load(new File(filePath)); - PDFRenderer pdfRenderer = new PDFRenderer(pdfDocument); - BufferedImage bufferedImage; - - int totalPages = pdfDocument.getNumberOfPages(); + private boolean isHashEqual(String first, String second) { + assert(first.split("_").length == 2); + String firstHash = first.split("_")[1]; + String secondHash = second.split("_")[1]; + return firstHash.equals(secondHash); + } - // Generate all images - for (int currentPage = 0; currentPage < totalPages; currentPage++) { - try { - bufferedImage = pdfRenderer.renderImageWithDPI(currentPage, 150, ImageType.RGB); - pages.add(SwingFXUtils.toFXImage(bufferedImage, null)); - } catch (IOException e) { - logger.info("PdfPanel: Page " + currentPage + " render failed"); - pages.add(null); - } + @Override + public int hashCode() { + return value.hashCode(); + } - // Stop thread and pass back if force unload - if (forceUnload) { - Platform.runLater(() -> imageLoaded(null, null)); - return; - } +} +``` +###### /java/seedu/address/model/person/Comment.java +``` java +/** + * Represents a Person's comment in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidComment(String)} + */ +public class Comment { - // Update on main thread - int currentLoaded = currentPage + 1; - Platform.runLater(() -> update(currentLoaded, totalPages)); - } + public static final String MESSAGE_COMMENT_CONSTRAINTS = "Person comment can take any values"; + public static final String COMMENT_VALIDATION_REGEX = ".*"; - // Pass back to main thread - Platform.runLater(() -> imageLoaded(pdfDocument, pages)); + public final String value; - } catch (IOException e) { - logger.info("PdfPanel: Load of file " + filePath + " failed"); - // Pass back to main thread - Platform.runLater(() -> imageLoaded(null, null)); + /** + * Constructs a {@code Comment}. + * + * @param comment A valid comment. + */ + public Comment(String comment) { + if (isNull(comment)) { + this.value = null; + } else { + checkArgument(isValidComment(comment), MESSAGE_COMMENT_CONSTRAINTS); + this.value = comment; } } /** - * Update status of the rendering so user know the status - * @param currentLoaded number of pages loaded - * @param totalPages of the PDF document + * Returns true if a given string is a valid comment. + * By default any string are valid */ - private void update(int currentLoaded, int totalPages) { - resumeLoadingLabel.setText("Loading page " + currentLoaded + " of " + totalPages); - resumeLoadingBar.setProgress((double) currentLoaded / (double) totalPages); + public static boolean isValidComment(String test) { + return test.matches(COMMENT_VALIDATION_REGEX); } - /** - * Callback from separate thread indicates all pages are loaded and rendered - * @param pdfDocument of the opened document - * @param pages An array of images of all the pages - */ - private void imageLoaded(PDDocument pdfDocument, ArrayList pages) { - this.pdfDocument = pdfDocument; - pdfPages = pages; + @Override + public String toString() { + return value; + } - if (pages == null) { - isLoaded = false; + @Override + public boolean equals(Object other) { + return other == this // Short circuit if same object + || (other instanceof Comment // instanceof handles nulls + && Objects.equals(this.value, ((Comment) other).value)); // State check + } - } else { - int totalPages = pdfPages.size(); + @Override + public int hashCode() { + return value.hashCode(); + } - // Setup all blank pages - for (int i = 0; i < totalPages; i++) { - // Wrap inside VBox for styling - VBox vBox = new VBox(); - vBox.getStyleClass().add("pdf-page"); - VBox.setMargin(vBox, new Insets(0, 0, 20, 0)); +} +``` +###### /java/seedu/address/commons/events/ui/InfoPanelChangedEvent.java +``` java +/** + * Indicates a change in Info Panel (Used for automated testing purpose) + */ +public class InfoPanelChangedEvent extends Event { + public static final EventType INFO_PANEL_EVENT = + new EventType<>("InfoPanelChangedEvent"); - // Setup VBox children (ImageView) - ImageView imageView = new ImageView(); - imageView.setPreserveRatio(true); - imageView.setCache(true); + public InfoPanelChangedEvent() { + this(INFO_PANEL_EVENT); + } - // Add into view - vBox.getChildren().add(imageView); - resumePanePages.getChildren().add(vBox); - } + public InfoPanelChangedEvent(EventType eventType) { + super(eventType); + } +} +``` +###### /java/seedu/address/commons/events/ui/MinimizeAppRequestEvent.java +``` java +/** + * Indicates a request for App minimize + */ +public class MinimizeAppRequestEvent extends BaseEvent { - // Initialize size and scroll detection - handleResizeEvent(); - handleScrollEvent(); + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} +``` +###### /java/seedu/address/commons/events/ui/MaximizeAppRequestEvent.java +``` java +/** + * Indicates a request for App minimize + */ +public class MaximizeAppRequestEvent extends BaseEvent { - // Set label to first page - resumePageLabel.setText(1 + " / " + totalPages); - resumePageLabel.setVisible(true); + @Override + public String toString() { + return this.getClass().getSimpleName(); + } +} +``` +###### /java/seedu/address/commons/events/ui/PersonChangedEvent.java +``` java +/** + * Indicates a person change in address book + */ +public class PersonChangedEvent extends BaseEvent { - isLoaded = true; - } + private final Person source; + private final Person target; - resumePane.setVisible(true); - resumeLoading.setVisible(false); - loading = false; + public PersonChangedEvent(Person source, Person target) { + this.source = source; + this.target = target; } - /** - * Unload the PDFPanel to free up resources - */ - public void unload() { - // Force unload enabled - forceUnload = true; - - // Retry later if loading - if (loading) { - Platform.runLater(this::unload); - return; - } + public Person getSource() { + return source; + } - // Only unload when it is imageLoaded - if (isLoaded) { - isLoaded = false; - - // Clear all array - pdfPages.clear(); - resumePanePages.getChildren().clear(); - - // Hide page - resumePageLabel.setVisible(false); - resumePane.setVisible(false); - - try { - if (pdfDocument != null) { - pdfDocument.close(); - } - } catch (IOException e) { - logger.info("PdfPanel: Unload failed"); - } - } - - forceUnload = false; + public Person getTarget() { + return target; } - /** - * Handle the resize event by resizing all pages - */ - private void handleResizeEvent() { - // Fit all images to width of the viewport - double width = resumePane.getWidth() - 40; - ObservableList childrens = resumePanePages.getChildren(); - - // Resize all the images - for (int i = 0; i < childrens.size(); i++) { - VBox vBox = (VBox) childrens.get(i); - ImageView imageView = (ImageView) vBox.getChildren().get(0); - Image page = pdfPages.get(i); - - // Size have to be fixed so it can maintain the size even when images outside of viewport are cleared - double aspectRatio = page.getWidth() / page.getHeight(); - imageView.setFitWidth(width); - imageView.setFitHeight(width / aspectRatio); - } + @Override + public String toString() { + return this.getClass().getSimpleName(); } +} +``` +###### /java/seedu/address/commons/events/ui/ShowPanelRequestEvent.java +``` java +/** + * Indicates a request for panel show + */ +public class ShowPanelRequestEvent extends BaseEvent { - /** - * Handle the scroll event to lazy load the images into ImageView for performance - */ - private void handleScrollEvent() { - ObservableList childrens = resumePanePages.getChildren(); - int totalPages = childrens.size(); - - // Compute view boundary (Only display image if visible) - Bounds viewBound = resumePane.localToScene(resumePane.getBoundsInLocal()); - double viewMinY = viewBound.getMinY(); - double viewMaxY = viewBound.getMaxY(); - double viewMid = (viewMinY + viewMaxY) / 2; - - for (int i = 0; i < totalPages; i++) { - - // Compute page boundary - VBox vBox = (VBox) childrens.get(i); - Bounds bounds = vBox.localToScene(vBox.getBoundsInLocal()); - - ImageView imageView = (ImageView) vBox.getChildren().get(0); - - // Check if page is visible in viewport - if (bounds.getMinY() < viewMaxY && bounds.getMaxY() > viewMinY) { - if (imageView.getImage() == null) { - imageView.setImage(pdfPages.get(i)); - } - } else { - imageView.setImage(null); - } + private final String panel; - // Update page number label - if (bounds.getMinY() < viewMid && bounds.getMaxY() > viewMid) { - resumePageLabel.setText((i + 1) + " / " + totalPages); - } - } + public ShowPanelRequestEvent(String panel) { + this.panel = panel; } - @Subscribe - private void handlePersonPanelSelectionChangedEvent(PersonPanelSelectionChangedEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - PersonCard selectedCard = event.getNewSelection(); - selectedPerson = (selectedCard == null) ? null : selectedCard.getPerson(); + public String getRequestedPanel() { + return panel; } - @Subscribe - private void handlePersonChangedEvent(PersonChangedEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - Person source = event.getSource(); - Person target = event.getTarget(); - - if (selectedPerson != null && selectedPerson.equals(source)) { - selectedPerson = target; - } + @Override + public String toString() { + return this.getClass().getSimpleName(); } } ``` -###### /java/seedu/address/ui/PersonCard.java +###### /java/seedu/address/commons/util/UiUtil.java ``` java - public PersonCard(Person person, int displayedIndex) { - super(FXML); - - this.person = person; - this.index = displayedIndex - 1; - - cardPersonPane.setOpacity(0); - updatePersonCard(); +/** + * Helper functions for handling UI information + */ +public class UiUtil { + public static final Interpolator EASE_OUT_CUBIC = Interpolator.SPLINE(0.215, 0.61, 0.355, 1); + private static final String FORMAT_DATE = "d MMM y"; + private static final String FORMAT_TIME = "hh:mm:ssa"; - registerAsAnEventHandler(this); + /** + * Convert double into string with {@code points} amount of decimal places + * @param decimal The double to be formatted + * @param points Number of decimal places + * @return the formatted string with {@code points} number of decimal places + */ + public static String toFixed(double decimal, int points) { + return toFixed(String.valueOf(decimal), points); } /** - * Update the person with the latest information - * It can be updated from address book change or selection change + * Convert string representation of decimal into string with {@code points} amount of decimal places + * @param decimal The string representation of decimal to be formatted + * @param points Number of decimal places + * @return the formatted string with {@code points} number of decimal places */ - private void updatePersonCard() { - if (person == null) { - return; - } - - cardPhoto.fitWidthProperty().bind(cardPhotoMask.widthProperty()); - cardPhoto.fitHeightProperty().bind(cardPhotoMask.heightProperty()); + public static String toFixed(String decimal, int points) { + double value = Double.parseDouble(decimal); + String pattern = "0"; - Image profileImage = person.getProfileImage().getImage(); - if (profileImage == null) { - cardPhoto.setImage(null); - } else { - try { - cardPhoto.setImage(profileImage); - } catch (Exception e) { - logger.info("Failed to load image file"); - } + if (points > 0) { + pattern += "."; + pattern += StringUtils.repeat("0", points); } - cardPersonName.setText(person.getName().fullName); - cardPersonUniversity.setText(person.getUniversity().value); - cardPersonEmail.setText(person.getEmail().value); - cardPersonContact.setText(person.getPhone().value); - cardPersonStatus.setText(person.getStatus().value); - cardPersonStatus.setStyle("-fx-background-color: " + UiUtil.colorToHex(person.getStatus().color)); - cardPersonNumber.setText(String.valueOf(index + 1)); - - double rating = person.getRating().overallScore; - if (rating < 1e-3) { - cardPersonRating.setText(""); - iconRating.setVisible(false); - } else { - cardPersonRating.setText(UiUtil.toFixed(rating, 2)); - iconRating.setVisible(true); - } + DecimalFormat df = new DecimalFormat(pattern); + return df.format(value); } /** - * Play animation (Fade is done on MainWindow) + * Convert JavaFX color into web hex color + * @param color to be converted + * @return the web hex String representation of the color */ - public void play() { - Animation fadeIn = UiUtil.fadeNode(cardPersonPane, true, MAX_ANIMATION_TIME_MS, 0, e -> {}); - fadeIn.setDelay(Duration.millis(index * 50)); - fadeIn.play(); + public static String colorToHex(Color color) { + return String.format("#%02X%02X%02X", + (int) (color.getRed() * 255), (int) (color.getGreen() * 255), (int) (color.getBlue() * 255)); } /** - * Show without animation (Fade is done on MainWindow) + * Fade in or fade out the node, then callback + * @param node to be faded in or out + * @param fadeIn If set, the fade will be fade in, otherwise it will be fade out + * @param from The opacity to start fading from + * @param maxDuration of the transition should be + * @param callback after the transition is done + * @return the {@code Animation} of the transition */ - public void show() { - cardPersonPane.setOpacity(1); - } - - @Subscribe - private void handlePersonChangedEvent(PersonChangedEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); + public static Animation fadeNode(Node node, boolean fadeIn, double from, + double maxDuration, EventHandler callback) { + Interpolator easing = fadeIn ? Interpolator.EASE_IN : Interpolator.EASE_OUT; + double to = fadeIn ? 1 : 0; + double duration = Math.max(1, Math.abs(from - to) * maxDuration); - Person source = event.getSource(); - Person target = event.getTarget(); + FadeTransition fade = new FadeTransition(Duration.millis(duration), node); + fade.setFromValue(from); + fade.setToValue(to); + fade.setCycleCount(1); + fade.setAutoReverse(false); + fade.setInterpolator(easing); + fade.setOnFinished(event -> { + if (Math.abs(node.getOpacity() - to) < 1e-3) { + callback.handle(event); + } + }); - if (person != null && person.equals(source)) { - person = target; - updatePersonCard(); - } + return fade; } -``` -###### /java/seedu/address/ui/TitleBar.java -``` java + /** - * Opens the help window. + * Fade in or fade out the node, then callback + * @param node to be faded in or out + * @param fadeIn If set, the fade will be fade in, otherwise it will be fade out + * @param maxDuration of the transition should be + * @param callback after the transition is done + * @return the {@code Animation} of the transition */ - @FXML - public void handleHelp(MouseEvent event) { - if (MouseButton.PRIMARY.equals(event.getButton())) { - HelpWindow helpWindow = new HelpWindow(); - helpWindow.show(); - } + public static Animation fadeNode(Node node, boolean fadeIn, + double maxDuration, EventHandler callback) { + double from = node.getOpacity(); + return fadeNode(node, fadeIn, from, maxDuration, callback); } /** - * Minimizes the application. + * Format date time to more readable format + * @param dateTime to be formatted + * @return the formatted date time */ - @FXML - private void handleMinimize(MouseEvent event) { - if (MouseButton.PRIMARY.equals(event.getButton())) { - raise(new MinimizeAppRequestEvent()); - } + public static String formatDate(LocalDateTime dateTime) { + String date = DateTimeFormatter.ofPattern(FORMAT_DATE, Locale.ENGLISH).format(dateTime); + String time = DateTimeFormatter.ofPattern(FORMAT_TIME, Locale.ENGLISH).format(dateTime).toLowerCase(); + return date + " " + time; } +} +``` +###### /java/seedu/address/logic/commands/ShowCommand.java +``` java +/** + * Shows a specific panel + */ +public class ShowCommand extends Command { + + public static final String COMMAND_WORD = "show"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Shows a specific panel. The panel can be either 'info' or 'resume'.\n" + + "Make sure person are selected before calling this command.\n" + + "When resume is requested, it will only shows when it is available.\n" + + "Parameters: PANEL (must be either 'info' or 'resume', case sensitive)\n" + + "Example: " + COMMAND_WORD + " info"; + + public static final String PANEL_INFO = "info"; + public static final String PANEL_RESUME = "resume"; + + public static final String MESSAGE_NOT_SELECTED = "A person must be selected before showing a panel."; + public static final String MESSAGE_RESUME_NA = "The selected person doesn't have a resume"; + public static final String MESSAGE_SHOW_SUCCESS = "Showing the requested panel"; /** - * Maximizes the application. + * Enumeration of acceptable panel */ - @FXML - private void handleMaximize(MouseEvent event) { - if (MouseButton.PRIMARY.equals(event.getButton())) { - raise(new MaximizeAppRequestEvent()); - } + public enum Panel { + INFO, RESUME } - /** - * Closes the application. - */ - @FXML - private void handleExit(MouseEvent event) { - if (MouseButton.PRIMARY.equals(event.getButton())) { - raise(new ExitAppRequestEvent()); - } + private final Panel panel; + + public ShowCommand(Panel panel) { + requireNonNull(panel); + this.panel = panel; } - @Subscribe - private void handleShowHelpEvent(ShowHelpRequestEvent event) { - logger.info(LogsCenter.getEventHandlingLogMessage(event)); - HelpWindow helpWindow = new HelpWindow(); - helpWindow.show(); + @Override + public CommandResult execute() throws CommandException { + Person selectedPerson = model.getSelectedPerson(); + if (selectedPerson == null) { + throw new CommandException(MESSAGE_NOT_SELECTED); + + } else { + switch (panel) { + case INFO: + EventsCenter.getInstance().post(new ShowPanelRequestEvent(InfoPanel.PANEL_NAME)); + break; + case RESUME: + if (selectedPerson.getResume().value != null) { + EventsCenter.getInstance().post(new ShowPanelRequestEvent(PdfPanel.PANEL_NAME)); + } else { + throw new CommandException(MESSAGE_RESUME_NA); + } + break; + default: + break; + } + + return new CommandResult(MESSAGE_SHOW_SUCCESS); + } } - @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)); + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ShowCommand // instanceof handles nulls + && this.panel.equals(((ShowCommand) other).panel)); // state check } } ``` -###### /java/seedu/address/ui/UiResizer.java +###### /java/seedu/address/logic/commands/InterviewCommand.java ``` java /** - * Ui Resizer, a utility to manage resize event of Stage such as resizable window + * Schedule interview of an existing person in the address book. */ -public class UiResizer { - - private Stage stage; +public class InterviewCommand extends UndoableCommand { - private double lastX; - private double lastY; - private double lastWidth; - private double lastHeight; + public static final String COMMAND_WORD = "interview"; - public UiResizer(Stage stage, GuiSettings guiSettings, double minWidth, double minHeight, int cornerSize) { - this.stage = stage; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Schedule interview for the person " + + "by the index number used in the last person listing. " + + "Existing scheduled date will be overwritten by the input value.\n" + + "Parameters: INDEX (must be a positive integer) " + + "DATETIME (parse by natural language)\n" + + "Example: " + COMMAND_WORD + " 1 next Friday at 3pm"; - // Set listeners - ResizeListener resizeListener = new ResizeListener(stage, minWidth, minHeight, cornerSize); - stage.getScene().addEventHandler(MouseEvent.MOUSE_MOVED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_PRESSED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_DRAGGED, resizeListener); - stage.getScene().addEventHandler(MouseEvent.MOUSE_RELEASED, resizeListener); + public static final String MESSAGE_INTERVIEW_PERSON_SUCCESS = + "Interview of person named %1$s has been scheduled on %2$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in HR+."; + public static final String PARSED_RESULT = "Parsed date: %1$s"; - // Set last value - lastX = guiSettings.getWindowCoordinates().x; - lastY = guiSettings.getWindowCoordinates().y; - lastWidth = guiSettings.getWindowWidth(); - lastHeight = guiSettings.getWindowHeight(); - } + private final Index index; + private final LocalDateTime dateTime; - private Rectangle2D getScreenBound() { - return Screen.getPrimary().getVisualBounds(); - } + private Person personToInterview; + private Person scheduledPerson; /** - * Maximize / Un-maximize the stage, polyfill for native {@link Stage#setMaximized} feature + * @param index of the person in the filtered person list to schedule interview + * @param dateTime of the interview */ - public void toggleMaximize() { - Rectangle2D screenBound = getScreenBound(); - double stageX = stage.getX(); - double stageY = stage.getY(); - double stageWidth = stage.getWidth(); - double stageHeight = stage.getHeight(); + public InterviewCommand(Index index, LocalDateTime dateTime) { + requireNonNull(index); + requireNonNull(dateTime); - if (stageWidth == screenBound.getWidth() && stageHeight == screenBound.getHeight()) { - stage.setX(lastX); - stage.setY(lastY); - stage.setWidth(lastWidth); - stage.setHeight(lastHeight); - } else { - lastX = stageX; - lastY = stageY; - lastWidth = stageWidth; - lastHeight = stageHeight; - stage.setX(screenBound.getMinX()); - stage.setY(screenBound.getMinY()); - stage.setWidth(screenBound.getWidth()); - stage.setHeight(screenBound.getHeight()); - } + this.index = index; + this.dateTime = dateTime; } - /** - * Manage the resize event during mouse move and drag - */ - static class ResizeListener implements EventHandler { - private Stage stage; - - private boolean holding = false; - private int cornerSize; + public Index getIndex() { + return index; + } - // Starting position of resizing - private double startX = 0; - private double startY = 0; + public LocalDateTime getDateTime() { + return dateTime; + } - // Min sizes for stage - private double minWidth; - private double minHeight; + public Person getPersonToInterview() { + return personToInterview; + } - public ResizeListener(Stage stage, double minWidth, double minHeight, int borderSize) { - this.stage = stage; - this.minWidth = minWidth; - this.minHeight = minHeight; - this.cornerSize = borderSize; + @Override + public CommandResult executeUndoableCommand() throws CommandException { + try { + model.updatePerson(personToInterview, scheduledPerson); + } catch (DuplicatePersonException dpe) { + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } catch (PersonNotFoundException pnfe) { + throw new AssertionError("The target person cannot be missing"); } - @Override - public void handle(MouseEvent mouseEvent) { - String eventType = mouseEvent.getEventType().getName(); - Scene scene = stage.getScene(); + return new CommandResult(String.format(MESSAGE_INTERVIEW_PERSON_SUCCESS, + scheduledPerson.getName(), UiUtil.formatDate(dateTime))); + } - double mouseX = mouseEvent.getSceneX(); - double mouseY = mouseEvent.getSceneY(); + @Override + protected void preprocessUndoableCommand() throws CommandException { + List lastShownList = model.getFilteredPersonList(); + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } - switch (eventType) { + personToInterview = lastShownList.get(index.getZeroBased()); + scheduledPerson = createScheduledPerson(personToInterview, dateTime); + } - case "MOUSE_MOVED": - scene.setCursor((isResizePosition(mouseX, mouseY) || holding) ? Cursor.SE_RESIZE : Cursor.DEFAULT); - break; + /** + * Creates and returns a {@code Person} with the details of {@code personToInterview} + * with updated with {@code dateTime}. + */ + private static Person createScheduledPerson(Person personToInterview, LocalDateTime dateTime) { + requireAllNonNull(personToInterview, dateTime); - case "MOUSE_RELEASED": - holding = false; - scene.setCursor(Cursor.DEFAULT); - break; + return new Person(personToInterview.getName(), personToInterview.getPhone(), personToInterview.getEmail(), + personToInterview.getAddress(), personToInterview.getUniversity(), + personToInterview.getExpectedGraduationYear(), + personToInterview.getMajor(), personToInterview.getGradePointAverage(), + personToInterview.getJobApplied(), personToInterview.getRating(), + personToInterview.getResume(), personToInterview.getProfileImage(), personToInterview.getComment(), + new InterviewDate(dateTime), personToInterview.getStatus(), + personToInterview.getTags()); + } - case "MOUSE_PRESSED": - // Left click only - if (MouseButton.PRIMARY.equals(mouseEvent.getButton())) { - holding = isResizePosition(mouseX, mouseY); - startX = stage.getWidth() - mouseX; - startY = stage.getHeight() - mouseY; - } - break; + @Override + public String getParsedResult() { + return String.format(PARSED_RESULT, UiUtil.formatDate(dateTime)); + } - case "MOUSE_DRAGGED": - if (holding) { - setStageWidth(mouseX + startX); - setStageHeight(mouseY + startY); - } - break; - - default: - - } - } - - /** - * Check if the X and Y coordinate of the mouse are in the range of draggable position - * - * @param x coordinate of the {@code MouseEvent} - * @param y coordinate of the {@code MouseEvent} - * @return {@code true} if the coordinate is in the range of draggable position, {@code false} otherwise - */ - private boolean isResizePosition(double x, double y) { - Scene scene = stage.getScene(); - return (x > scene.getWidth() - cornerSize && y > scene.getHeight() - cornerSize); - } - - /** - * Set the width of the stage, with validation to be larger than {@code minWidth} - * - * @param width of the stage - */ - private void setStageWidth(double width) { - stage.setWidth(Math.max(width, minWidth)); + @Override + public boolean equals(Object other) { + // Short circuit if same object + if (other == this) { + return true; } - /** - * Set the height of the stage, with validation to be larger than {@code minHeight} - * - * @param height of the stage - */ - private void setStageHeight(double height) { - stage.setHeight(Math.max(height, minHeight)); + // instanceof handles nulls + if (!(other instanceof InterviewCommand)) { + return false; } + // State check + InterviewCommand i = (InterviewCommand) other; + return getIndex().equals(i.getIndex()) + && getDateTime().equals(i.getDateTime()) + && Objects.equals(getPersonToInterview(), i.getPersonToInterview()); } } ``` -###### /resources/view/CommandBox.fxml -``` fxml - - - - - - -``` -###### /resources/view/HRTheme.css -``` css - -/* Fonts */ +###### /java/seedu/address/logic/parser/ShowCommandParser.java +``` java +/** + * Parses input arguments and creates a new ShowCommand object + */ +public class ShowCommandParser implements Parser { -@font-face { - src: url("../fonts/Roboto-Thin.ttf"); -} + /** + * Parses the given {@code String} of arguments in the context of the ShowCommand + * and returns an ShowCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ShowCommand parse(String args) throws ParseException { + requireNonNull(args); -@font-face { - src: url("../fonts/Roboto-ThinItalic.ttf"); -} + // Parse the arguments + String requestedString = args.trim(); + ShowCommand.Panel requestedPanel = parsePanel(requestedString); + return new ShowCommand(requestedPanel); + } -@font-face { - src: url("../fonts/Roboto-Light.ttf"); -} + /** + * Parses {@code panel} into a {@code ShowCommand.Panel} and returns it. + * Leading and trailing whitespaces will be trimmed. + * @throws ParseException if the specified panel is invalid (not info or resume). + */ + private ShowCommand.Panel parsePanel(String panel) throws ParseException { + String trimmed = panel.trim(); -@font-face { - src: url("../fonts/Roboto-LightItalic.ttf"); + switch (trimmed) { + case ShowCommand.PANEL_INFO: + return ShowCommand.Panel.INFO; + case ShowCommand.PANEL_RESUME: + return ShowCommand.Panel.RESUME; + default: + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ShowCommand.MESSAGE_USAGE)); + } + } } +``` +###### /java/seedu/address/logic/parser/ParserUtil.java +``` java + /** + * Parses a {@code String profileImage} into a {@code ProfileImage}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code profileImage} is invalid. + */ + public static ProfileImage parseProfileImage(String profileImage) throws IllegalValueException { + requireNonNull(profileImage); + String trimmedProfileImage = profileImage.trim(); + if (!ProfileImage.isValidFile(trimmedProfileImage)) { + throw new IllegalValueException(ProfileImage.MESSAGE_IMAGE_CONSTRAINTS); + } + return new ProfileImage(trimmedProfileImage); + } -@font-face { - src: url("../fonts/Roboto-Regular.ttf"); -} + /** + * Parses a {@code Optional profileImage} into an {@code Optional} + * if {@code profileImage} is present. + * See header comment of this class regarding the use of {@code Optional} parameters. + */ + public static Optional parseProfileImage(Optional profileImage) throws IllegalValueException { + requireNonNull(profileImage); + return profileImage.isPresent() ? Optional.of(parseProfileImage(profileImage.get())) : Optional.empty(); + } -@font-face { - src: url("../fonts/Roboto-Italic.ttf"); -} + /** + * Parses a {@code String comment} into a {@code Comment}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws IllegalValueException if the given {@code comment} is invalid. + */ + public static Comment parseComment(String comment) throws IllegalValueException { + requireNonNull(comment); + String trimmedComment = comment.trim(); + if (!Comment.isValidComment(trimmedComment)) { + throw new IllegalValueException(Comment.MESSAGE_COMMENT_CONSTRAINTS); + } + return new Comment(trimmedComment); + } -@font-face { - src: url("../fonts/Roboto-Medium.ttf"); + /** + * Parses a {@code Optional comment} into an {@code Optional} if {@code comment} is present. + * See header comment of this class regarding the use of {@code Comment} parameters. + */ + public static Optional parseComment(Optional comment) throws IllegalValueException { + requireNonNull(comment); + return comment.isPresent() ? Optional.of(parseComment(comment.get())) : Optional.empty(); + } } +``` +###### /java/seedu/address/logic/parser/InterviewCommandParser.java +``` java +/** + * Parses input arguments and creates a new InterviewCommand object + */ +public class InterviewCommandParser implements Parser { -@font-face { - src: url("../fonts/Roboto-MediumItalic.ttf"); -} + public static final String MESSAGE_DATETIME_PARSE_FAIL = "Failed to parse the date time from the string: %1$s"; + private static final com.joestelmach.natty.Parser parser = new com.joestelmach.natty.Parser(); -@font-face { - src: url("../fonts/Roboto-Bold.ttf"); -} + /** + * Parses the given {@code String} of arguments in the context of the InterviewCommand + * and returns an InterviewCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public InterviewCommand parse(String args) throws ParseException { + try { + // Parse the arguments + String[] arguments = args.trim().split("\\s+", 2); + if (arguments.length != 2) { + throw new IllegalValueException("Invalid command, expected 2 arguments"); + } -@font-face { - src: url("../fonts/Roboto-BoldItalic.ttf"); -} + // Parse the index + Index index = ParserUtil.parseIndex(arguments[0]); -@font-face { - src: url("../fonts/Roboto-Black.ttf"); -} + // Parse the date time + LocalDateTime dateTime = parseDateFromNaturalLanguage(arguments[1]); -@font-face { - src: url("../fonts/Roboto-BlackItalic.ttf"); -} + return new InterviewCommand(index, dateTime); + } catch (ParseException pe) { + throw pe; -/* Reset / General */ + } catch (IllegalValueException ive) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, InterviewCommand.MESSAGE_USAGE)); + } + } -* { - -fx-box-border: transparent !important; -} + /** + * Parses the given natural language {@code String} and returns a {@code LocalDateTime} object + * that represents the English representation of the date and time + * @throws ParseException if the phrase cannot be converted to date and time + */ + private LocalDateTime parseDateFromNaturalLanguage(String naturalLanguage) throws ParseException { + List groups = parser.parse(naturalLanguage); + if (groups.size() < 1) { + throw new ParseException(String.format(MESSAGE_DATETIME_PARSE_FAIL, naturalLanguage)); + } -/* Split bar */ + List dates = groups.get(0).getDates(); + if (dates.size() < 1) { + throw new ParseException(String.format(MESSAGE_DATETIME_PARSE_FAIL, naturalLanguage)); + } -.split-pane > .split-pane-divider { - -fx-padding: 4; - -fx-background-color: #E8E8E8; - -fx-background-insets: 5 3 5 3; + Date date = dates.get(0); + return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + } } +``` +###### /java/seedu/address/logic/parser/EditCommandParser.java +``` java + /** + * Parses {@code Optional profileImage} into a {@code Optional} + * if {@code profileImage} is non-empty. + * If profile image is present and equals to empty string, it will be parsed into a + * {@code ProfileImage} containing null value. + */ + private Optional parseProfileImageForEdit(Optional profileImage) + throws IllegalValueException { + assert profileImage != null; + if (!profileImage.isPresent()) { + return Optional.empty(); + } + if (profileImage.get().equals("")) { + return Optional.of(new ProfileImage(null)); + } else { + return ParserUtil.parseProfileImage(profileImage); + } + } -/* Scroll bar */ - -.scroll-bar { - -fx-background-color: #E8E8E8; - -fx-background-insets: 0; - -fx-background-radius: 10; - -fx-padding: 0; -} - -.scroll-bar > .thumb { - -fx-background-color: #D7D7D7; - -fx-background-insets: 0; - -fx-background-radius: 10; -} - -.scroll-bar:horizontal .increment-button, -.scroll-bar:horizontal .decrement-button { - -fx-background-color: transparent; - -fx-padding: 5 0 5 0; -} - -.scroll-bar:vertical .increment-button, -.scroll-bar:vertical .decrement-button { - -fx-background-color: transparent; - -fx-padding: 0 5 0 5; -} - -.scroll-bar .increment-arrow, -.scroll-bar .decrement-arrow { - -fx-shape: ""; - -fx-padding: 0; -} - -.corner { - -fx-background-color: transparent; -} - -/* Dialog pane */ - -.dialog-pane { - -fx-background-color: white; -} - -.dialog-pane:header .header-panel { - -fx-background-color: linear-gradient(to right, #42A5F5, #1976D2); - -fx-background-insets: 0; -} - -.dialog-pane:header .header-panel .label { - -fx-text-fill: white; - -fx-font-family: "Roboto"; -} - - - -/* Root */ - -#mainWindow { - -fx-background-color: white; -} - -/* Window border */ - -#bottomWrapper { - -fx-border-color: #ccc; - -fx-border-width: 0 1 1 1; -} - -#topCommandPlaceholder { - -fx-border-color: #ccc; - -fx-border-width: 0 1 0 0; -} - - - -/* Top Wrapper + Pane */ - -#topPane { - -fx-background-color: linear-gradient(to right, #42A5F5, #1976D2); -} - -#styleTopPane { - -fx-background-color: white; -} - -/* Title Bar */ - -#topStatusMessage { - -fx-font-family: "Roboto Light"; - -fx-font-size: 14; - -fx-opacity: 0.8; - -fx-text-fill: white; -} - -#topStatusFile { - -fx-background-color: white; - -fx-background-radius: 8; - - -fx-border-color: white; - -fx-border-width: 4 10 4 10; - -fx-border-radius: 8; - - -fx-font-family: "Roboto Light"; - -fx-font-size: 13; - -fx-text-fill: #616161; - -fx-text-overrun: leading-ellipsis; -} - -/* Controls */ - -#controlHelpInner, #controlMinimizeInner, -#controlMaximize, #controlClose { - -fx-background-color: white; -} - -#controlHelp, #controlMinimize, -#controlMaximize, #controlClose { - -fx-opacity: 0.5; -} - -#controlHelp:hover, #controlMinimize:hover, -#controlMaximize:hover, #controlClose:hover { - -fx-opacity: 0.9; -} - -/* Command Box */ - -#topCommand { - -fx-background-color: white; -} - -#commandInput { - -fx-border-color: white; - -fx-background-color: #E0E0E0; - -fx-background-radius: 8; - -fx-font-family: "Roboto"; - -fx-font-size: 13; - -fx-text-fill: #616161; -} - -#commandInput.command-error { - -fx-text-fill: #F44336; -} - -#commandInput:focused { - -fx-background-color: #D8D8D8; -} - -/* Result Box */ - -#commandResult { - -fx-background-color: white; - -fx-border-color: #BCBCBC; - -fx-background-radius: 0; - -fx-font-family: "Roboto"; - -fx-font-size: 13; -} - -#commandResult .content { - -fx-cursor: text; - -fx-background-color: white; -} - -#commandResult:focused { - -fx-border-color: #42A5F5; -} - -#commandResult:focused .content { - -fx-background-color: #F5F5F5; -} - -/* Bottom */ - -#welcomePane, #bottomPaneSplit, #resumePane { - -fx-background-color: white; -} - -/* List */ - -#listPersons { - -fx-background-color: white; -} - -#listPersons .list-cell, -#listPersons .list-cell:selected { - -fx-background-color: transparent; -} - -/* Card */ - -#cardPersonPane { - -fx-padding: 5 5 5 0; -} - -#cardPerson { - -fx-background-color: white; - -fx-border-color: white; - -fx-border-width: 1; - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.25), 12, 0, 0, 0); -} - -#cardPerson > * { - -fx-opacity: 0.5; -} - -#listPersons .list-cell:selected #cardPerson > * { - -fx-opacity: 1; -} - -#listPersons .list-cell:selected #cardPerson { - -fx-border-color: #42A5F5; -} - -#listPersons:focused #cardPerson { - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.4), 14, 0, 0, 0); -} - -#listPersons:focused .list-cell:selected #cardPerson { - -fx-effect: dropshadow(gaussian, #64B5F6, 14, 0, 0, 0); -} - -#cardPhotoPane { - -fx-background-color: #E8E8E8; -} - -#cardPhotoMask { - -fx-background-color: white; -} - -/* Card Info */ - -#cardPersonInfo .label, -#listPersons .list-cell:selected #cardPersonInfo .label { - -fx-text-fill: black; -} - -#cardPersonName { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 15; -} - -#cardPersonUniversity { - -fx-font-family: "Roboto Light"; - -fx-font-size: 11; - -fx-opacity: 0.9; -} - -#cardPersonEmail { - -fx-font-family: "Roboto Light"; - -fx-font-size: 10; - -fx-opacity: 0.8; -} - -#cardPersonContact { - -fx-font-family: "Roboto Light"; - -fx-font-size: 10; - -fx-opacity: 0.8; -} - -/* Card Rating */ - -#cardPersonRatingPane .label, -#listPersons .list-cell:selected #cardPersonRatingPane .label { - -fx-text-fill: black; -} - -#cardPersonRating { - -fx-font-family: "Roboto Light"; - -fx-font-size: 11; - -fx-opacity: 0.8; -} - -#iconRating { - -fx-background-color: #FFC107; -} - -/* Card Status */ - -#cardPersonStatus { - -fx-background-color: #8BC34A; - -fx-background-radius: 50; - - -fx-font-family: "Roboto Medium"; - -fx-font-size: 10; - -fx-text-fill: white; -} - -/* Card Number */ - -#cardPersonNumberPane .label, -#listPersons .list-cell:selected #cardPersonNumberPane .label { - -fx-text-fill: white; -} - -#cardPersonNumber { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 13; -} - -#cardPersonNumber, #styleCardPersonNumber { - -fx-background-color: #BCBCBC; -} - -#listPersons .list-cell:selected #cardPersonNumber, -#listPersons .list-cell:selected #styleCardPersonNumber { - -fx-background-color: #42A5F5; -} - -/* Welcome */ - -#welcomeTitle { - -fx-font-family: "Roboto Bold"; - -fx-font-size: 18; -} - -#welcomeMadeWith { - -fx-font-family: "Roboto Light"; - -fx-font-size: 13; - -fx-opacity: 0.5; -} - -#welcomeName { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 13; - -fx-opacity: 0.8; -} - - - -/* Resume */ - -#resumePanePages { - -fx-background-color: white; -} - -.pdf-page { - -fx-background-color: white; - -fx-border-width: 1; - -fx-border-color: #ddd; - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.25), 12, 0, 0, 2); -} - -#resumePane:focused .pdf-page { - -fx-border-color: #aaa; - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.35), 18, 0, 0, 3); -} - -#resumePageLabel { - -fx-background-color: #2196F3; - -fx-text-fill: white; - -fx-background-radius: 100; - -fx-opacity: 0.6; -} - -#resumeLoading { - -fx-background-color: white; -} - -#resumePageLabel, #resumeLoadingLabel { - -fx-font-family: "Roboto"; -} - - - -/* Info */ - -#infoMain, #infoMainPart, #infoMainPane, #infoSplitPane, #infoSplitMainPane { - -fx-background-color: white; -} - -#infoMain { - -fx-border-width: 1 0 1 1; - -fx-border-color: white; -} - -#infoMainPane:focused #infoMain, -#infoSplitMainPane:focused #infoMain { - -fx-border-color: #90CAF9; -} - -#infoMainName { - -fx-font-family: "Roboto Black"; - -fx-font-size: 20; - -fx-text-fill: #64b5f6; -} - -#infoMainUniversity, #infoMainMajorYear { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 11; -} - -/* Info CGPA */ - -#infoMainCgpaLabel { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 11; -} - -#infoMainCgpa { - -fx-background-color: #64B5F6; - -fx-background-radius: 100; - - -fx-font-family: "Roboto Bold"; - -fx-font-size: 13; - -fx-text-fill: white; -} - -/* Info Contact */ - -#iconEmail, #iconAddress, #iconPhone { - -fx-background-color: #BDBDBD; -} - -#infoMainEmail, #infoMainAddress, #infoMainPhone { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 11; -} - -/* Info Interview */ - -#infoMainPositionLabel, #infoMainStatusLabel { - -fx-font-family: "Roboto Light"; - -fx-font-size: 11; -} - -#infoMainPosition, #infoMainStatus { - -fx-font-family: "Roboto Bold"; - -fx-font-size: 12; -} - -#infoMainStatus { - -fx-text-fill: #8bc34a; -} - -#infoMainInterviewDateCalendar { - -fx-border-color: #B4B4B4; - -fx-border-radius: 10; -} - -#infoMainInterviewMonth { - -fx-background-color: #64B5F6; - -fx-background-radius: 9 9 0 0; - - -fx-font-family: "Roboto Bold"; - -fx-font-size: 12; - -fx-text-fill: white; -} - -#infoMainInterviewDate { - -fx-font-family: "Roboto Bold"; - -fx-font-size: 32; -} - -#infoMainInterviewDay { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 11; -} - -#infoMainInterviewTime { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 11; -} - -/* Info comments */ - -#infoMainCommentsLabel { - -fx-font-family: "Roboto Light"; - -fx-font-size: 12; -} - -#infoMainComments { - -fx-background-color: #E8E8E8; - -fx-background-radius: 8; - - -fx-font-family: "Roboto Medium"; - -fx-font-size: 12; -} - -/* Side graph */ - -#infoSideGraph { - -fx-background-color: white; -} - -#infoRatingTechnicalLabel, #infoRatingCommunicationLabel, -#infoRatingProblemSolvingLabel, #infoRatingExperienceLabel, -#infoRatingOverallLabel { - -fx-font-family: "Roboto Medium"; - -fx-font-size: 11; -} - -#infoRatingTechnicalValue, #infoRatingCommunicationValue, -#infoRatingProblemSolvingValue, #infoRatingExperienceValue, -#infoRatingOverallValue { - -fx-font-family: "Roboto"; - -fx-font-size: 11; -} - -.hr-progress-bar > .bar { - -fx-background-color: #2196F3; - -fx-background-insets: 0; - -fx-background-radius: 0; - -fx-padding: 2; -} - -.hr-progress-bar > .track { - -fx-background-color: #BBDEFB; - -fx-background-insets: 0; - -fx-background-radius: 0; -} - -/* Side button */ - -.hr-button { - -fx-background-color: #42A5F5; - -fx-background-insets: 0; - - -fx-border-radius: 0; - -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.4), 12, 0, 0, 3); - - -fx-font-family: "Roboto Medium"; - -fx-font-size: 13; - -fx-text-fill: white; - -fx-cursor: hand; -} - -.hr-button:focused, -.hr-button:hover { - -fx-background-color: #1976D2; -} - -.hr-button:pressed { - -fx-background-color: #0D47A1; -} - -.hr-button:disabled { - -fx-background-color: #616161; -} - - - -/* Logos */ - -#logoTop { - -fx-background-color: white; -} - -#logoWelcome { - -fx-background-color: linear-gradient(to right, #42A5F5, #1976D2); -} - - - -/* Floating */ - -#floatParseIcon { - -fx-background-color: #999; -} - -#floatParseLabel { - -fx-text-fill: #999; - -fx-font-family: "Roboto"; - -fx-font-size: 13; -} - -#floatParseRealTime { - -fx-background-color: rgba(248, 248, 248, 0.95); - -fx-border-width: 1; - -fx-border-color: #ddd; -} - -#floatParseRealTime.parse-valid #floatParseIcon { - -fx-background-color: "#64B5F6"; -} - -#floatParseRealTime.parse-invalid #floatParseIcon { - -fx-background-color: "#E57373"; -} - -#floatParseRealTime.parse-valid #floatParseLabel { - -fx-text-fill: "#64B5F6"; -} - -#floatParseRealTime.parse-invalid #floatParseLabel { - -fx-text-fill: "#E57373"; -} - - - -/* SVG Graphics */ - -.icon-help { - -fx-shape: "M4.4,7.6c0-0.2,0-0.3,0-0.4c0-0.5,0.1-0.9,0.2-1.2c0.1-0.3,0.3-0.5,0.5-0.8C5.2,5,5.5,4.7,5.9,4.4C6.3,4,6.6,3.7,6.7,3.5C6.9,3.3,6.9,3,6.9,2.8c0-0.5-0.2-0.9-0.5-1.2C6,1.2,5.6,1,5,1C4.5,1,4.1,1.2,3.7,1.5S3.2,2.3,3.1,3L1.8,2.8c0.1-0.9,0.4-1.6,1-2.1S4.1,0,5,0c1,0,1.7,0.3,2.3,0.8s0.9,1.2,0.9,1.9c0,0.4-0.1,0.8-0.3,1.2C7.7,4.2,7.3,4.7,6.7,5.2C6.3,5.5,6,5.8,5.9,6C5.8,6.1,5.7,6.3,5.7,6.5c-0.1,0.2-0.1,0.6-0.1,1H4.4z M4.3,10V8.6h1.4V10H4.3z"; -} - -.icon-expand { - -fx-shape: "M0,0v10h10V0H0z M9,9H1V1h8V9z"; -} - -.icon-close { - -fx-shape: "M5.9,5L10,9.1L9.1,10L5,5.9L0.9,10L0,9.1L4.1,5L0,0.9L0.9,0L5,4.1L9.1,0L10,0.9L5.9,5z"; -} - -.icon-star { - -fx-shape: "M4,0.2l0.9,2.9H8L5.5,4.9l1,2.9L4,6L1.5,7.8l1-2.9L0,3.1h3.1L4,0.2z"; -} - -.icon-email { - -fx-shape: "M9.1,2.6L10,1.9v6.4c0,0.3-0.2,0.5-0.5,0.5H0.4C0.2,8.8,0,8.6,0,8.3V1.9l0.9,0.7l3.8,2.8c0.1,0,0.2,0.1,0.3,0.1s0.2,0,0.3-0.1L9.1,2.6z M5,4.4l4.4-3.2H0.6L5,4.4z"; -} - -.icon-address { - -fx-shape: "M5,0C3.3,0,1.9,1.4,1.9,3.1S3.4,7.2,5,10c1.6-2.8,3.1-5.1,3.1-6.9S6.7,0,5,0z M5,4.4c-0.7,0-1.2-0.6-1.2-1.2S4.3,1.9,5,1.9s1.2,0.6,1.2,1.2S5.7,4.4,5,4.4z"; -} - -.icon-phone { - -fx-shape: "M10,7.9c0,0.1,0,0.3-0.1,0.5S9.8,8.8,9.8,8.9C9.7,9.1,9.4,9.4,8.9,9.6C8.5,9.9,8,10,7.6,10c-0.1,0-0.3,0-0.4,0c-0.1,0-0.3,0-0.4-0.1c-0.2,0-0.3-0.1-0.3-0.1c-0.1,0-0.2-0.1-0.4-0.1S5.8,9.5,5.7,9.5C5.3,9.3,4.9,9.1,4.5,8.9C3.9,8.5,3.3,8,2.6,7.4S1.5,6.1,1.1,5.5C0.9,5.1,0.7,4.7,0.5,4.3c0,0-0.1-0.2-0.1-0.3S0.2,3.6,0.2,3.5c0-0.1-0.1-0.2-0.1-0.3S0,2.9,0,2.8c0-0.1,0-0.2,0-0.4C0,2,0.1,1.5,0.4,1.1c0.3-0.5,0.5-0.8,0.8-0.9c0.1-0.1,0.3-0.1,0.5-0.1S2,0,2.1,0c0.1,0,0.1,0,0.1,0c0.1,0,0.2,0.2,0.4,0.5c0.1,0.1,0.1,0.2,0.2,0.4C2.9,1.1,3,1.3,3.1,1.4s0.1,0.3,0.2,0.4c0,0,0.1,0.1,0.1,0.2c0.1,0.1,0.1,0.2,0.2,0.3c0,0.1,0,0.1,0,0.2c0,0.1-0.1,0.2-0.2,0.4C3.3,2.9,3.2,3,3,3.2C2.8,3.3,2.7,3.4,2.6,3.5C2.4,3.7,2.4,3.8,2.4,3.9c0,0,0,0.1,0,0.2c0,0.1,0,0.1,0.1,0.1c0,0,0,0.1,0.1,0.2c0,0.1,0.1,0.1,0.1,0.1C3,5.1,3.4,5.7,3.9,6.1c0.5,0.5,1,0.9,1.7,1.2c0,0,0.1,0,0.1,0.1c0.1,0,0.1,0.1,0.2,0.1c0,0,0.1,0,0.1,0.1c0.1,0,0.1,0,0.2,0c0.1,0,0.2-0.1,0.3-0.2S6.7,7.2,6.8,7s0.2-0.3,0.4-0.4c0.1-0.1,0.3-0.2,0.4-0.2c0.1,0,0.1,0,0.2,0c0.1,0,0.2,0.1,0.3,0.2c0.1,0.1,0.2,0.1,0.2,0.1c0.1,0.1,0.2,0.1,0.4,0.2s0.3,0.2,0.5,0.2s0.3,0.2,0.4,0.2C9.8,7.5,10,7.7,10,7.7C10,7.8,10,7.8,10,7.9z"; -} - -.style-lower-triangle { - -fx-shape: "M1,0v1H0L1,0z"; -} - -.style-upper-triangle { - -fx-shape: "M0,1V0h1L0,1z"; -} - -.style-circle { - -fx-shape: "M2,1c0,0.6-0.4,1-1,1S0,1.6,0,1s0.4-1,1-1S2,0.4,2,1z"; -} - -.style-mask-circle { - -fx-shape: "M1,2H0V1C0,1.6,0.4,2,1,2z M1,2h1V1C2,1.6,1.6,2,1,2z M1,0H0v1C0,0.4,0.4,0,1,0z M1,0c0.6,0,1,0.4,1,1V0H1z"; -} - -.logo-hr { - -fx-shape: "M106.5,0.3v181.5H86.2v-80.6H20.3v80.6H0V0.3h20.3v80.6h65.9V0.3H106.5z M124,2.1v21.4c5.3-2.2,11.1-3.4,17.2-3.4c25,0,45.3,20.3,45.3,45.3c0,6-1.2,11.8-3.3,17C180,84.8,175.3,87,169,87c-14.2,0-19.5-12-19.7-12.6c-0.7-1.8-2.8-2.7-4.6-1.9c-1.8,0.7-2.7,2.8-1.9,4.6c0.3,0.7,7.1,17,26.2,17c2.9,0,5.6-0.4,8-1c-8.3,10.6-21.2,17.5-35.7,17.5c-6.1,0-11.9-1.2-17.2-3.4v21.4c5.5,1.5,11.3,2.3,17.2,2.3c8,0,15.7-1.4,22.8-4.1c5.7,7.6,16.4,25.3,20.7,55.1h20.5c-4.2-33.1-15.6-54.2-23.4-65.2c15.2-12,25-30.6,25-51.4c0-33.9-25.9-61.9-58.9-65.2h-13.4C131,0.5,127.4,1.1,124,2.1z M164,59.5c-3.9,0-7.1-3.2-7.1-7.1c0-3.9,3.2-7.1,7.1-7.1s7.1,3.2,7.1,7.1C171.1,56.4,167.9,59.5,164,59.5z M256,112.3h-15.2V97.1h-15.2v15.2h-15.2v15.2h15.2v15.2h15.2v-15.2H256V112.3z"; -} + /** + * Parses {@code Optional comment} into a {@code Optional} if {@code comment} is non-empty. + */ + private Optional parseCommentForEdit(Optional comment) throws IllegalValueException { + assert comment != null; + if (!comment.isPresent()) { + return Optional.empty(); + } + if (comment.get().equals("")) { + return Optional.of(new Comment(null)); + } else { + return ParserUtil.parseComment(comment); + } + } ``` ###### /resources/view/InfoPanel.fxml ``` fxml @@ -3088,6 +2424,36 @@ public class UiResizer { ``` +###### /resources/view/TitleBar.fxml +``` fxml + + +``` ###### /resources/view/MainWindow.fxml ``` fxml @@ -3148,43 +2514,768 @@ public class UiResizer { ``` -###### /resources/view/PdfPanel.fxml +###### /resources/view/PdfPanel.fxml +``` fxml + + + + + + + + + + + + + + + + + + + + + + + +``` +###### /resources/view/PersonListPanel.fxml +``` fxml + +``` +###### /resources/view/ResultDisplay.fxml +``` fxml + + + + +