diff --git a/src/main/java/de/doubleslash/keeptime/controller/HeimatController.java b/src/main/java/de/doubleslash/keeptime/controller/HeimatController.java index 0e4df426..bead911e 100644 --- a/src/main/java/de/doubleslash/keeptime/controller/HeimatController.java +++ b/src/main/java/de/doubleslash/keeptime/controller/HeimatController.java @@ -26,15 +26,14 @@ import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask; import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTime; import de.doubleslash.keeptime.view.ProjectReport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; @Service public class HeimatController { @@ -210,27 +209,30 @@ public List getTableRows(final LocalDate currentReportDate, final List< final HeimatTask heimatTask = optionalHeimatTask.get(); taskName = heimatTask.name() + "\n" + heimatTask.taskHolderName(); } - - final Mapping mapping = new Mapping(id, true, false, - StyledMessage.of(new StyledMessage.TextSegment("Not mapped in KeepTime\n\n" + taskName)), "", times, - + StyledMessage syncMessage = StyledMessage.of( + new StyledMessage.TextSegment("Not mapped in KeepTime\n\n"), + new StyledMessage.TextSegment(taskName, true)); + final Mapping mapping = new Mapping(id, true, false,syncMessage, "", times, new ArrayList<>(0), heimatNotes, "", heimatTimeSeconds, 0); list.add(mapping); }); taskIdToHeimatTimesMap.forEach((id, times) -> { - final Optional mapping = mappedProjects.stream() + final List mappings = mappedProjects.stream() .filter(mp -> mp.getExternalTaskId() == id) - .findAny(); - if (mapping.isEmpty()) + .toList(); + if (mappings.isEmpty()) return; - final ExternalProjectMapping externalProjectMapping = mapping.get(); - final Optional optionalProject = workedProjectsSet.stream() + + Optional anyMatch = mappings.stream().filter(externalProjectMapping ->{ + final List optionalProject = workedProjectsSet.stream() .filter(wp -> wp.getId() == externalProjectMapping.getProject() - .getId()) - .findAny(); - if (optionalProject.isPresent()) { + .getId()).toList(); + return !optionalProject.isEmpty(); + }).findAny(); + + if (anyMatch.isPresent()) { return; } String heimatNotes = addHeimatNotes(times); @@ -238,8 +240,8 @@ public List getTableRows(final LocalDate currentReportDate, final List< StyledMessage syncMessage = StyledMessage.of( new StyledMessage.TextSegment("Present in Heimat but not KeepTime\n\nSync to "), - new StyledMessage.TextSegment(externalProjectMapping.getExternalTaskName(), true), - new StyledMessage.TextSegment("\n(" + externalProjectMapping.getExternalProjectName() + ")")); + new StyledMessage.TextSegment(mappings.get(0).getExternalTaskName(), true), + new StyledMessage.TextSegment("\n(" + mappings.get(0).getExternalProjectName() + ")")); final Mapping mapping2 = new Mapping(id, true, false, syncMessage, "", times, mappedProjects.stream() .filter( @@ -402,18 +404,20 @@ public ExistingAndInvalidMappings getExistingProjectMappings(List ex == p.getId()) .findAny(); if (mapping.isEmpty()) { - return new ProjectMapping(p, null); + return new ProjectMapping(p, null, false); } + final Optional any = externalProjects.stream() .filter(ep -> ep.id() == mapping.get().getExternalTaskId()) .findAny(); if (any.isEmpty()) { + ExternalProjectMapping existingMapping = mapping.get(); LOG.warn("A mapping exists but task does not exist anymore in Heimat! '{}'->'{}'.", - mapping.get().getProject(), mapping.get().getExternalTaskId()); - invalidExternalMappings.add(mapping.get()); - return new ProjectMapping(p, null); + existingMapping.getProject(), existingMapping.getExternalTaskId()); + invalidExternalMappings.add(existingMapping); + return new ProjectMapping(p, getHeimatTaskFromMapping(existingMapping),true); } - return new ProjectMapping(p, any.get()); + return new ProjectMapping(p, any.get(), false); }).toList(); final List invalidMappingsAsString = invalidExternalMappings.stream() @@ -437,6 +441,19 @@ public ExistingAndInvalidMappings getExistingProjectMappings(List ex return new ExistingAndInvalidMappings(validProjectMappings, invalidMappingsAsString); } + private HeimatTask getHeimatTaskFromMapping(ExternalProjectMapping mapping) { + try { + String json = mapping.getExternalTaskMetadata(); + if (json == null || json.isEmpty()) { + return null; + } + return objectMapper.readValue(json, HeimatTask.class); + } catch (Exception e) { + LOG.warn("Unable to deserialize HeimatTask from mapping metadata", e); + return null; + } + } + public record UserMapping(Mapping mapping, boolean shouldSync, String userNotes, int userMinutes) {} public record Mapping(long heimatTaskId, boolean canBeSynced, boolean shouldBeSynced, StyledMessage syncMessage, @@ -448,10 +465,12 @@ public record HeimatErrors(UserMapping mapping, String errorMessage) {} public static class ProjectMapping { private Project project; private HeimatTask heimatTask; + private boolean pendingRemoval; - public ProjectMapping(final Project project, final HeimatTask heimatTask) { + public ProjectMapping(final Project project, final HeimatTask heimatTask, boolean pendingRemoval) { this.project = project; this.heimatTask = heimatTask; + this.pendingRemoval = pendingRemoval; } public Project getProject() { @@ -469,5 +488,13 @@ public HeimatTask getHeimatTask() { public void setHeimatTask(final HeimatTask heimatTask) { this.heimatTask = heimatTask; } + + public boolean isPendingRemoval() { + return pendingRemoval; + } + + public void setPendingRemoval(boolean pendingRemoval) { + this.pendingRemoval = pendingRemoval; + } } } diff --git a/src/main/java/de/doubleslash/keeptime/rest/integration/heimat/JwtDecoder.java b/src/main/java/de/doubleslash/keeptime/rest/integration/heimat/JwtDecoder.java index c5a903d4..3eca3430 100644 --- a/src/main/java/de/doubleslash/keeptime/rest/integration/heimat/JwtDecoder.java +++ b/src/main/java/de/doubleslash/keeptime/rest/integration/heimat/JwtDecoder.java @@ -30,7 +30,7 @@ public record JWTTokenAttributes( String header, String payload, LocalDateTime expiration - ) {} + ) { } public static JWTTokenAttributes parse(String bearerToken) { @@ -57,6 +57,10 @@ public static JWTTokenAttributes parse(String bearerToken) { return new JWTTokenAttributes(header, payload, expiration); } + public static boolean isExpired(JWTTokenAttributes token, LocalDateTime localDateTimeNow) { + return localDateTimeNow.isAfter(token.expiration); + } + private static String removeBearerPrefix(String token) { return token.startsWith("Bearer ") ? token.substring(7) : token; } diff --git a/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsMapController.java b/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsMapController.java index 7124493d..5aa30930 100644 --- a/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsMapController.java +++ b/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsMapController.java @@ -24,6 +24,11 @@ import de.doubleslash.keeptime.model.Project; import de.doubleslash.keeptime.rest.integration.heimat.model.ExistingAndInvalidMappings; import de.doubleslash.keeptime.rest.integration.heimat.model.HeimatTask; +import de.doubleslash.keeptime.viewpopup.SearchCombobox; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; @@ -33,18 +38,28 @@ import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.fxml.FXML; -import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.DatePicker; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; import javafx.scene.control.cell.CheckBoxTableCell; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import java.time.LocalDate; -import java.util.List; -import java.util.Optional; - @Component public class ExternalProjectsMapController { @@ -86,15 +101,27 @@ private void initialize() { tasksForDateDatePicker.setValue(LocalDate.now()); tasksForDateDatePicker.setDisable(true); // TODO add listener on this thing - // but what happens with mapped projects not existing at that date? but actually not related to this feature alone final List externalProjects = heimatController.getTasks(tasksForDateDatePicker.getValue()); - final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings( - externalProjects); - final List previousProjectMappings = existingAndInvalidMappings.validMappings(); + final ExistingAndInvalidMappings existingAndInvalidMappings = heimatController.getExistingProjectMappings(externalProjects); + + final List previousProjectMappings = existingAndInvalidMappings.validMappings(); final ObservableList newProjectMappings = FXCollections.observableArrayList( previousProjectMappings); + + Platform.runLater(() -> { + List warnings = existingAndInvalidMappings.invalidMappingsAsString(); + if (!warnings.isEmpty()) { + if (showInvalidMappingsDialog(warnings)) { + newProjectMappings.stream() + .filter(HeimatController.ProjectMapping::isPendingRemoval) + .forEach(pm -> pm.setHeimatTask(null)); + mappingTableView.refresh(); + } + } + }); + final FilteredList value = new FilteredList<>(newProjectMappings, pm -> pm.getProject().isWork()); mappingTableView.setItems(value); @@ -103,58 +130,62 @@ private void initialize() { TableColumn keepTimeColumn = new TableColumn<>("KeepTime project"); keepTimeColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getProject().getName())); + keepTimeColumn.setCellFactory(col -> new TableCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setTooltip(null); + } else { + setText(item); + Tooltip tooltip = new Tooltip(item); + setTooltip(tooltip); + } + } + }); + // External Project column with dropdown final ObservableList externalProjectsObservableList = FXCollections.observableArrayList( externalProjects); - externalProjectsObservableList.add(0, null); // option to clear selection - + externalProjectsObservableList.add(0,null); TableColumn externalColumn = new TableColumn<>("Heimat project"); externalColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue().getHeimatTask())); externalColumn.setCellFactory(col -> new TableCell<>() { - // TODO search in box would be nice - private final ComboBox comboBox = new ComboBox<>(externalProjectsObservableList); + private final SearchCombobox searchPopup = new SearchCombobox<>(externalProjectsObservableList); + + { + searchPopup.setDisplayTextFunction(ht -> ht == null ? "" : ht.taskHolderName() + " - " + ht.name()); + searchPopup.setClearFieldAfterSelection(false); + searchPopup.setPromptText("Search Project..."); + searchPopup.setOnItemSelected((selectedTask, popup) -> { + HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex()); + mapping.setHeimatTask(selectedTask); + if(selectedTask != null) + searchPopup.setComboBoxTooltip(selectedTask.name() + " - " + selectedTask.id()); + updateItem(selectedTask, false); + }); + } @Override protected void updateItem(HeimatTask item, boolean empty) { super.updateItem(item, empty); - // selected item - comboBox.setButtonCell(new ListCell<>() { - @Override - protected void updateItem(HeimatTask item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - setText(null); - } else { - setText(item.taskHolderName() + " - " + item.name()); - } - } - }); - - // Dropdown - comboBox.setCellFactory(param -> new ListCell<>() { - @Override - protected void updateItem(HeimatTask item, boolean empty) { - super.updateItem(item, empty); - if (item == null || empty) { - setGraphic(null); - setText(null); - } else { - // TODO maybe show if the project was already mapped - setText(item.taskHolderName() + " - " + item.name()); - } - } - }); - if (empty) { setGraphic(null); setText(null); + setStyle(null); } else { - comboBox.setValue(getTableView().getItems().get(getIndex()).getHeimatTask()); - comboBox.setOnAction(e -> { - HeimatController.ProjectMapping mapping = getTableView().getItems().get(getIndex()); - mapping.setHeimatTask(comboBox.getValue()); - }); - setGraphic(comboBox); + searchPopup.setSelectedItem(item); + if (item != null) { + searchPopup.setComboBoxTooltip(item.name() + " - " + item.id()); + } else { + searchPopup.setComboBoxTooltip(""); + } + + // highlight mappings which do not exist anymore + final String highlightStyle = item != null && !externalProjects.contains(item) ? "-fx-background-color: lightsalmon;" : null; + setStyle(highlightStyle); + setGraphic(searchPopup.getComboBox()); setText(null); } } @@ -179,7 +210,7 @@ protected void updateItem(HeimatTask item, boolean empty) { final Project project = controller.addNewProject( new Project(toBeCreatedHeimatTask.name() + " - " + toBeCreatedHeimatTask.taskHolderName(), toBeCreatedHeimatTask.bookingHint(), ColorHelper.randomColor(), true, sortIndex)); - newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask)); + newProjectMappings.add(new HeimatController.ProjectMapping(project, toBeCreatedHeimatTask, false)); } }); @@ -189,11 +220,6 @@ protected void updateItem(HeimatTask item, boolean empty) { }); cancelButton.setOnAction(ae -> thisStage.close()); - - List warnings = existingAndInvalidMappings.invalidMappingsAsString(); - if (!warnings.isEmpty()) { - Platform.runLater(() -> showInvalidMappingsDialog(warnings)); - } } private List showMultiSelectDialog(final List externalProjects, @@ -210,6 +236,11 @@ private List showMultiSelectDialog(final List externalPr ButtonType cancelButtonType = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); dialog.getDialogPane().getButtonTypes().addAll(okButtonType, cancelButtonType); + // Observable and filtered list + ObservableList baseList = FXCollections.observableArrayList(externalProjects); + FilteredList filteredList = new FilteredList<>(baseList, t -> true); + + // Name Column TableView tableView = new TableView<>(); TableColumn nameColumn = new TableColumn<>("Heimat project"); nameColumn.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue())); @@ -238,9 +269,10 @@ protected void updateItem(HeimatTask item, boolean empty) { tableView.setEditable(false); tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - tableView.setItems(FXCollections.observableArrayList(externalProjects)); + tableView.setItems(filteredList); - Button selectAllUnmappedButton = new Button("Select unmapped projects (" + unmappedHeimatTasks.size() + ")"); + Button selectAllUnmappedButton = new Button("Select unmapped projects (" + + unmappedHeimatTasks.size() + ")"); selectAllUnmappedButton.getStyleClass().add("secondary-button"); selectAllUnmappedButton.setOnAction(e -> { tableView.getSelectionModel().clearSelection(); @@ -250,7 +282,27 @@ protected void updateItem(HeimatTask item, boolean empty) { tableView.requestFocus(); }); - VBox content = new VBox(10, selectAllUnmappedButton, tableView); + TextField searchField = new TextField(); + searchField.setPromptText("Search..."); + searchField.textProperty().addListener((obs, oldText, newText) -> { + String filter = newText == null ? "" : newText.trim().toLowerCase(); + filteredList.setPredicate(task -> { + if (filter.isEmpty()) return true; + return task.taskHolderName().toLowerCase().contains(filter) + || task.name().toLowerCase().contains(filter); + }); + + long visibleUnmapped = filteredList.stream().filter(unmappedHeimatTasks::contains).count(); + selectAllUnmappedButton.setText("Select unmapped projects (" + + visibleUnmapped + ")"); + }); + searchField.getStyleClass().add("text-field"); + searchField.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(searchField, Priority.ALWAYS); + + HBox headContent = new HBox(50, selectAllUnmappedButton, searchField); + + VBox content = new VBox(10, headContent, tableView); dialog.getDialogPane().setContent(content); final List emptyList = List.of(); dialog.setResultConverter(dialogButton -> { @@ -279,11 +331,17 @@ protected void updateItem(HeimatTask item, boolean empty) { return result.orElse(emptyList); } - private void showInvalidMappingsDialog(final List warnings) { - Dialog dialog = new Dialog<>(); + private boolean showInvalidMappingsDialog(final List warnings) { + Dialog dialog = new Dialog<>(); + dialog.initOwner(this.thisStage); + + Stage dialogStage = (Stage) dialog.getDialogPane().getScene().getWindow(); + dialogStage.getIcons().addAll(this.thisStage.getIcons()); + dialog.setTitle("Invalid mappings"); - dialog.setHeaderText("Please note to following issue:"); + dialog.setHeaderText("The following projects are no longer available.\n" + + "Would you like to remove them from your mapping list?"); VBox warningBox = new VBox(10); for (String warning : warnings) { @@ -299,10 +357,11 @@ private void showInvalidMappingsDialog(final List warnings) { dialog.getDialogPane().setContent(scrollPane); dialog.getDialogPane().setMinWidth(400); - // Add OK button - ButtonType okButton = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE); - dialog.getDialogPane().getButtonTypes().add(okButton); + ButtonType removeButton = new ButtonType("Remove", ButtonBar.ButtonData.YES); + ButtonType keepButton = new ButtonType("Keep", ButtonBar.ButtonData.NO); + dialog.getDialogPane().getButtonTypes().setAll(removeButton, keepButton); - dialog.showAndWait(); + Optional result = dialog.showAndWait(); + return result.isPresent() && result.get() == removeButton; } } diff --git a/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsSyncController.java b/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsSyncController.java index 987a8f6b..3a37af68 100644 --- a/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsSyncController.java +++ b/src/main/java/de/doubleslash/keeptime/view/ExternalProjectsSyncController.java @@ -138,6 +138,7 @@ public class ExternalProjectsSyncController { private LocalDate currentReportDate; private Stage thisStage; + private Timeline closingTimeline; private final HeimatController heimatController; private final RotateTransition loadingSpinnerAnimation = new RotateTransition(Duration.seconds(1), syncingIconRegion); @@ -229,8 +230,10 @@ public void initForDate(LocalDate currentReportDate, List currentWorkItems itemsForBindings.add(addedRow); // add new row also to items2 - as it is not added automatically :( mappingTableView.scrollTo(items.size() - 1); // scroll to newly added row }); - heimatTaskSearchCombobox.setClearFieldAfterSelection(true); + heimatTaskSearchCombobox.setClearFieldAfterSelection(true); + heimatTaskSearchCombobox.setMaxSuggestionHeight(220); + heimatTaskSearchCombobox.setPromptText("Select Project..."); heimatTaskSearchContainer.getChildren().add(heimatTaskSearchCombobox.getComboBox()); HBox.setHgrow(heimatTaskSearchCombobox.getComboBox(), Priority.ALWAYS); } @@ -506,10 +509,14 @@ protected List call() { loadingSuccess); } + if (closingTimeline != null) { + closingTimeline.stop(); + } + final AtomicInteger remainingSeconds = new AtomicInteger(closingSeconds); loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds..."); loadingClosingMessage.setVisible(true); - Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> { + closingTimeline = new Timeline(new KeyFrame(Duration.seconds(1), event -> { remainingSeconds.getAndDecrement(); loadingClosingMessage.setText("Closing in " + remainingSeconds + " seconds..."); if (remainingSeconds.get() <= 0) { @@ -518,8 +525,8 @@ protected List call() { loadingClosingMessage.setVisible(false); } })); - timeline.setCycleCount(remainingSeconds.get()); - timeline.play(); + closingTimeline.setCycleCount(remainingSeconds.get()); + closingTimeline.play(); }); task.setOnFailed(e -> { @@ -702,6 +709,14 @@ public static LocalTime decrementToNextHour(LocalTime time) { public void setStage(final Stage thisStage) { this.thisStage = thisStage; + + thisStage.setOnCloseRequest(e -> { + if (closingTimeline != null) { + closingTimeline.stop(); + closingTimeline = null; + } + }); + registerKeyEventListenersForSpinners(thisStage); } diff --git a/src/main/java/de/doubleslash/keeptime/view/SettingsController.java b/src/main/java/de/doubleslash/keeptime/view/SettingsController.java index 4167cd28..e0cf5835 100644 --- a/src/main/java/de/doubleslash/keeptime/view/SettingsController.java +++ b/src/main/java/de/doubleslash/keeptime/view/SettingsController.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.nio.file.Paths; import java.sql.SQLException; +import java.time.LocalDateTime; import java.util.Comparator; import java.util.HashMap; import java.util.Map; @@ -213,7 +214,7 @@ public class SettingsController { private PasswordField heimatPatTextField; @FXML - private Label heimatExpiresLabel; + private Label expirationDateLabel; @FXML private Button heimatValidateConnectionButton; @@ -411,9 +412,20 @@ private void initializeHeimat() { heimatPatTextField.textProperty().addListener((observable, oldValue, newValue)->{ try{ final JwtDecoder.JWTTokenAttributes jwt = JwtDecoder.parse(newValue); - heimatExpiresLabel.setText(jwt.expiration().toString()); + final boolean isExpired = JwtDecoder.isExpired(jwt, LocalDateTime.now()); + final String expirationDate = jwt.expiration().toString(); + + if (isExpired) { + expirationDateLabel.setText(expirationDate + " (Expired)"); + expirationDateLabel.setTextFill(Color.RED); + } else { + expirationDateLabel.setText(expirationDate); + expirationDateLabel.setTextFill(Color.BLACK); + } + } catch(Exception e){ - heimatExpiresLabel.setText("Does not seem to be valid"); + expirationDateLabel.setText("Does not seem to be valid"); + expirationDateLabel.setTextFill(Color.RED); } }); heimatValidateConnectionLabel.setText("Not validated."); @@ -462,27 +474,27 @@ private void updateHeimatSettings() { private void showMapProjectsStage() { try{ // Settings stage - final FXMLLoader fxmlLoader2 = createFXMLLoader(RESOURCE.FXML_EXT_PROJECT_MAPPING); - fxmlLoader2.setControllerFactory(model.getSpringContext()::getBean); - final Parent settingsRoot = fxmlLoader2.load(); - ExternalProjectsMapController settingsController = fxmlLoader2.getController(); - Stage settingsStage = new Stage(); - settingsController.setStage(settingsStage); - settingsStage.initModality(Modality.APPLICATION_MODAL); - settingsStage.setTitle("External Project Mappings"); - settingsStage.setResizable(false); - settingsStage.getIcons().add(new Image(Resources.getResource(RESOURCE.ICON_MAIN).toString())); - - final Scene settingsScene = new Scene(settingsRoot); - settingsScene.setOnKeyPressed(ke -> { + final FXMLLoader fxmlLoader = createFXMLLoader(RESOURCE.FXML_EXT_PROJECT_MAPPING); + fxmlLoader.setControllerFactory(model.getSpringContext()::getBean); + final Parent externalProjectRoot = fxmlLoader.load(); + ExternalProjectsMapController externalProjectsMapController = fxmlLoader.getController(); + Stage externalProjectMappingStage = new Stage(); + externalProjectsMapController.setStage(externalProjectMappingStage); + externalProjectMappingStage.initModality(Modality.APPLICATION_MODAL); + externalProjectMappingStage.setTitle("External Project Mappings"); + externalProjectMappingStage.setResizable(false); + externalProjectMappingStage.getIcons().add(new Image(Resources.getResource(RESOURCE.ICON_MAIN).toString())); + + final Scene externalProjectMappingScene = new Scene(externalProjectRoot); + externalProjectMappingScene.setOnKeyPressed(ke -> { if (ke.getCode() == KeyCode.ESCAPE) { LOG.info("pressed ESCAPE"); - settingsStage.close(); + externalProjectMappingStage.close(); } }); - settingsStage.setScene(settingsScene); - settingsStage.showAndWait(); + externalProjectMappingStage.setScene(externalProjectMappingScene); + externalProjectMappingStage.showAndWait(); } catch (final Exception e) { throw new FXMLLoaderException(e); } diff --git a/src/main/java/de/doubleslash/keeptime/viewpopup/SearchCombobox.java b/src/main/java/de/doubleslash/keeptime/viewpopup/SearchCombobox.java index 8e6bcc03..ee79ebb6 100644 --- a/src/main/java/de/doubleslash/keeptime/viewpopup/SearchCombobox.java +++ b/src/main/java/de/doubleslash/keeptime/viewpopup/SearchCombobox.java @@ -33,7 +33,8 @@ public class SearchCombobox { private String promptText = "Select item…"; private double maxSuggestionHeight = 200; - private BiConsumer> onItemSelected = (item, popup) -> {}; + private BiConsumer> onItemSelected = (item, popup) -> { + }; private boolean clearFieldAfterSelection = false; public SearchCombobox(ObservableList items) { @@ -64,20 +65,22 @@ private void setupUI() { suggestionList.setCellFactory(listView -> new ListCell<>() { private final Label label = new Label(); private final StackPane pane = new StackPane(label); + { label.setWrapText(true); - label.setStyle("-fx-padding: 5;"); + label.setStyle("-fx-padding: 2;"); pane.setAlignment(Pos.CENTER_LEFT); pane.setMinWidth(0); pane.setPrefWidth(1); } + @Override protected void updateItem(T item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) { + if (empty) { setGraphic(null); } else { - label.setText(displayTextFunction.apply(item)); + label.setText(item == null ? "-" : displayTextFunction.apply(item)); setGraphic(pane); } } @@ -91,14 +94,15 @@ private void setupListeners() { }); ChangeListener hidePopupListener = (obs, was, isNow) -> { - if (!searchField.isFocused() && !suggestionList.isFocused()) popup.hide(); + if (!searchField.isFocused() && !suggestionList.isFocused()) + popup.hide(); }; searchField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { if (isNowFocused && !clearFieldAfterSelection) { filterList(""); // Show all items show(searchField); - searchField.selectAll(); // <--- This line selects all text! + searchField.selectAll(); } }); @@ -127,7 +131,7 @@ private void setupListeners() { suggestionList.setOnKeyPressed(ev -> { if (ev.getCode() == KeyCode.ENTER) { T selected = suggestionList.getSelectionModel().getSelectedItem(); - if (selected != null) handleSelection(selected); + handleSelection(selected); } else if (ev.getCode() == KeyCode.UP && suggestionList.getSelectionModel().getSelectedIndex() == 0) { searchField.requestFocus(); } else if (ev.getCode() == KeyCode.ESCAPE) { @@ -138,21 +142,20 @@ private void setupListeners() { suggestionList.setOnMouseClicked(ev -> { T selected = suggestionList.getSelectionModel().getSelectedItem(); - if (selected != null) handleSelection(selected); + handleSelection(selected); }); - searchField.textProperty().addListener((obs, oldText, newText) -> { - filterList(newText); - }); + searchField.textProperty().addListener((obs, oldText, newText) -> filterList(newText)); } private void filterList(String input) { String filter = (input == null) ? "" : input.trim().toLowerCase(); - ObservableList filtered = FXCollections.observableArrayList( - allItems.stream() - .filter(item -> displayTextFunction.apply(item).toLowerCase().contains(filter)) - .collect(Collectors.toList()) - ); + ObservableList filtered = FXCollections.observableArrayList(allItems.stream() + .filter(item -> { + if(item == null) return filter.isEmpty(); + return displayTextFunction.apply( + item).toLowerCase().contains(filter);}) + .collect(Collectors.toList())); suggestionList.setItems(filtered); if (!filtered.isEmpty() && searchField.isFocused()) { show(searchField); @@ -196,14 +199,13 @@ public void setMaxSuggestionHeight(double height) { suggestionList.setMaxHeight(height); } - public HBox getComboBox() { - return container; - } + public HBox getComboBox() {return container;} public void show(Node owner) { - if (owner == null || suggestionList.getItems().isEmpty()) return; + if (owner == null || suggestionList.getItems().isEmpty()) + return; Bounds bounds = owner.localToScreen(owner.getBoundsInLocal()); - suggestionList.setPrefWidth(searchField.getWidth()); + suggestionList.setPrefWidth(container.getWidth()); popup.show(owner, bounds.getMinX(), bounds.getMaxY()); } @@ -221,38 +223,44 @@ public void setSelectedItem(T item) { public T getSelectedItem() { String text = searchField.getText(); for (T item : allItems) { - if (displayTextFunction.apply(item).equals(text)) return item; + if (displayTextFunction.apply(item).equals(text)) + return item; } return null; } - public TextField getSearchField() { - return searchField; - } + public TextField getSearchField() {return searchField;} - public ListView getSuggestionList() { - return suggestionList; - } + public ListView getSuggestionList() {return suggestionList;} - public Button getShowSuggestionsButton() { - return showSuggestionsButton; - } + public Button getShowSuggestionsButton() {return showSuggestionsButton;} - public Function getDisplayTextFunction() { - return displayTextFunction; - } + public Function getDisplayTextFunction() {return displayTextFunction;} public void setOnItemSelected(BiConsumer> handler) { - this.onItemSelected = handler != null ? handler : (item, popup) -> {}; + this.onItemSelected = handler != null ? handler : (item, popup) -> { + }; } - public void setClearFieldAfterSelection(boolean c) { - this.clearFieldAfterSelection = c; - } + public void setClearFieldAfterSelection(boolean c) {this.clearFieldAfterSelection = c;} public void clear() { searchField.clear(); if (!promptText.isEmpty()) searchField.setPromptText(promptText); } + + public void setComboBoxTooltip(String tooltipText) { + if (tooltipText != null && !tooltipText.isBlank()) { + Tooltip tooltip = new Tooltip(tooltipText); + + Tooltip.install(container, tooltip); + Tooltip.install(searchField, tooltip); + Tooltip.install(showSuggestionsButton, tooltip); + } else { + Tooltip.uninstall(container, null); + Tooltip.uninstall(searchField, null); + Tooltip.uninstall(showSuggestionsButton, null); + } + } } \ No newline at end of file diff --git a/src/main/resources/layouts/externalProjectSync.fxml b/src/main/resources/layouts/externalProjectSync.fxml index ce878723..7e5adfab 100644 --- a/src/main/resources/layouts/externalProjectSync.fxml +++ b/src/main/resources/layouts/externalProjectSync.fxml @@ -1,5 +1,6 @@ + @@ -8,7 +9,7 @@ - + @@ -42,17 +43,20 @@ - + - - + +