import java.io.*; import java.nio.file.*; import java.nio.charset.*; import java.util.*; import java.util.function.*; import java.util.Map.*; import java.util.AbstractMap.*; import java.text.*; import java.time.*; import javafx.application.*; import javafx.stage.*; import javafx.scene.*; import javafx.scene.image.*; import javafx.scene.control.*; import javafx.scene.layout.*; import javafx.scene.text.*; import javafx.scene.input.*; import javafx.scene.canvas.*; import javafx.scene.paint.*; import javafx.collections.*; import javafx.collections.transformation.*; import javafx.beans.*; import javafx.beans.property.*; import javafx.geometry.*; import javafx.event.*; public class VCardEdit extends Application { TableView<VCard> tableView; ObservableList<VCard> data = null; ObservableList<String> oblCharset = null; boolean nf = false; SortedList<VCard> sortedList; TableRow<VCard> currentRow = null; ContextMenu contextMenu = new ContextMenu(); FileChooser.ExtensionFilter efVcf = new FileChooser.ExtensionFilter("vCard (*.vcf, *.vcard)", "*.vcf", "*.vcard"); FileChooser.ExtensionFilter efImgs = new FileChooser.ExtensionFilter("Supported image formats (*.jpg, *.jpeg, *.png, *.gif)", "*.jpg", "*.jpeg", "*.png", "*.gif"); FileChooser.ExtensionFilter efAll = new FileChooser.ExtensionFilter("All files (*.*)", "*.*"); BooleanProperty bprModified = new SimpleBooleanProperty(false); StringProperty sprFNane = new SimpleStringProperty(""); Charset charset = Charset.defaultCharset(); int csSpecify = 0; Label lsb1, lsb2; Stage stage; File appRootPath; @Override public void start(Stage stage) { this.stage = stage; try { String sarp = getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); if (getClass().getResource("/" + getClass().getName() + ".class").toString().startsWith("jar:")) { sarp = sarp.substring(0, sarp.lastIndexOf("/") + 1); } appRootPath = new File(sarp); } catch (Exception ex) { appRootPath = new File(""); } BorderPane layout = new BorderPane(); stage.setScene(new Scene(layout, 700, 400)); lsb1 = new Label(""); lsb1.setPrefWidth(150); lsb2 = new Label(charset.name()); MenuBar menuBar = new MenuBar(); Menu miFile = new Menu("File"); menuBar.setBackground(Background.EMPTY); MenuItem miFileNew = new MenuItem("New"); miFileNew.setOnAction(ae -> { if (confirmSave()) { data.clear(); sprFNane.set(""); bprModified.set(false); nf = false; } }); MenuItem miFileOpen = new MenuItem("Open..."); miFileOpen.setOnAction(ae -> { if (confirmSave()) load(null, true); }); MenuItem miFileAppendFrom = new MenuItem("Append from..."); miFileAppendFrom.setOnAction(ae -> { load(null, false); }); MenuItem miFileSave = new MenuItem("Save"); miFileSave.setOnAction(ae -> { save(false); }); MenuItem miFileSaveAs = new MenuItem("Save as..."); miFileSaveAs.setOnAction(ae -> { save(true); }); MenuItem miFileOptions = new MenuItem("Options..."); miFileOptions.setOnAction(ae -> { options(); }); MenuItem miFileExit = new MenuItem("Exit"); miFileExit.setOnAction(ae -> { if (confirmSave()) { Platform.exit(); System.exit(0); } }); miFile.getItems().addAll(miFileNew, miFileOpen, miFileAppendFrom, miFileSave, miFileSaveAs, miFileOptions, new SeparatorMenuItem(), miFileExit); menuBar.getMenus().addAll(miFile); miFile.setOnShowing(e -> { miFileSave.setDisable(!bprModified.get() || nf || sprFNane.get().isEmpty()); }); BorderPane topLayout = new BorderPane(); topLayout.setLeft(menuBar); topLayout.setRight(search()); layout.setTop(topLayout); vCardTable(); layout.setCenter(tableView); HBox hBox = new HBox(lsb1, lsb2); hBox.setPadding(new Insets(2, 0, 2, 6)); hBox.setOnMouseClicked(me -> { if (me.getButton() == MouseButton.PRIMARY && me.getClickCount() == 2) options(); }); layout.setBottom(hBox); stage.setOnCloseRequest(e -> { if (!confirmSave()) e.consume(); }); stage.show(); Platform.runLater(() -> { List<String> p = getParameters().getRaw(); if (!p.isEmpty()) { File pFile = new File(p.get(0)).getAbsoluteFile(); try { pFile = pFile.getCanonicalFile(); } catch (Exception ex) { } if (pFile.isFile()) load(pFile, true); // new Thread(() -> { load(pFile, true); }).start(); else alert(Alert.AlertType.CONFIRMATION, "File " + pFile.getName() + " not found.", stage, ButtonType.OK); } tableView.requestFocus(); }); } boolean confirmSave() { if (bprModified.get()) { ButtonType bttp = alert(Alert.AlertType.CONFIRMATION, "File is changed. Save?", stage, ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); if (bttp == null || bttp == ButtonType.CANCEL) return false; if (bttp == ButtonType.NO) return true; return save(false); } return true; } boolean load(File openFile, boolean v) { if (openFile == null) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open (" + charset.name()+")"); fileChooser.getExtensionFilters().addAll(efVcf, efAll); fileChooser.setInitialDirectory(appRootPath.isDirectory() ? appRootPath : null); openFile = fileChooser.showOpenDialog(stage); if (openFile == null) return false; } appRootPath = openFile.getParentFile(); try (InputStream in = Files.newInputStream(openFile.toPath())) { if (v) data.clear(); data.addAll(VCard.read(in, charset)); tableView.refresh(); tableView.getSelectionModel().selectFirst(); if (v) { sprFNane.set(openFile.getName()); nf = true; } bprModified.set(!v); return true; } catch (Exception ex) { alert(Alert.AlertType.ERROR, ex.toString(), stage, ButtonType.OK); } return false; } boolean save(boolean v) { File saveFile; if (v || nf || sprFNane.get().isEmpty() || !appRootPath.isDirectory()) { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save as (" + charset.name()+")"); fileChooser.getExtensionFilters().addAll(efAll, efVcf); fileChooser.setInitialDirectory(appRootPath.isDirectory() ? appRootPath : null); fileChooser.setInitialFileName(sprFNane.get().isEmpty() ? "vCard.vcf" : sprFNane.get()); saveFile = fileChooser.showSaveDialog(stage); if (saveFile == null) return false; appRootPath = saveFile.getParentFile(); } else saveFile = new File(appRootPath, sprFNane.get()); try (OutputStream out = Files.newOutputStream(saveFile.toPath())) { VCard.write(out, sortedList, charset, csSpecify); bprModified.set(false); sprFNane.set(saveFile.getName()); nf = false; return true; } catch (Exception ex) { alert(Alert.AlertType.ERROR, ex.toString(), stage, ButtonType.OK); } return false; } void vCardTable() { InvalidationListener listener = obs -> { stage.setTitle((sprFNane.get().isEmpty() ? "New" : sprFNane.get()) + (bprModified.get() ? " *" : "")); }; bprModified.addListener(listener); sprFNane.addListener(listener); listener.invalidated(null); Font fontBold = Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize() + 3); tableView = new TableView<>(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new Label(null)); data = FXCollections.observableArrayList(); sortedList = new SortedList<>(data); sortedList.comparatorProperty().bind(new SimpleObjectProperty<Comparator<VCard>>((t1, t2) -> t1.compareTo(t2))); tableView.setItems(sortedList); TableColumn<VCard, VCard> column = new TableColumn<>(); column.setCellValueFactory(c -> new ReadOnlyObjectWrapper<VCard>(c.getValue())); column.setCellFactory(c -> new TableCell<VCard, VCard>() { Node emptyPhoto = crPict(null, 0); @Override public void updateItem(VCard item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) setGraphic(null); else { if (item.obj == null) { Image img = crImg(item.photo); item.obj = new SimpleEntry<Image, Node>(img, (img == null ? null : crPict(img, 0))); } @SuppressWarnings("unchecked") Entry<Image, Node> ent = (Entry<Image, Node>) item.obj; Node pict = (ent.getKey() == null ? emptyPhoto : ent.getValue()); GridPane gridPane = new GridPane(); gridPane.setHgap(20); gridPane.setPadding(new Insets(1, 10, 1, 10)); gridPane.add(pict, 0, 0, 1, 4); Label l = new Label(item.fname); l.setMaxWidth(400); l.setFont(fontBold); gridPane.add(l, 1, 0, 3, 1); String[] st = new String[] { item.name[0], item.name[1], item.name[2] }; for (int i = 0; i < 3; i++) { Label label; label = new Label(st[i]); label.setMinWidth(130); label.setMaxWidth(130); gridPane.add(label, 1, i + 1); String s; s = (item.tel.size() > i ? (String)item.tel.get(i).value : ""); label = new Label(s); label.setMinWidth(150); label.setMaxWidth(150); gridPane.add(label, 2, i + 1); s = (item.email.size() > i ? (String)item.email.get(i).value : ""); label = new Label(s); label.setMinWidth(200); label.setMaxWidth(200); gridPane.add(label, 3, i + 1); } setGraphic(gridPane); } } }); tableView.getColumns().add(column); MenuItem miEdit = new MenuItem("Edit"); miEdit.setOnAction(ae -> { edit(0); }); MenuItem miView = new MenuItem("View"); miView.setOnAction(ae -> { view(); }); MenuItem miIns = new MenuItem("Insert"); miIns.setOnAction(ae -> { edit(1); }); MenuItem miDel = new MenuItem("Delete"); miDel.setOnAction(ae -> { delete(); }); contextMenu.getItems().setAll(miEdit, miView, miIns, new SeparatorMenuItem(), miDel); tableView.setRowFactory(tv -> { TableRow<VCard> row = new TableRow<>(); row.setOnMouseClicked(me -> { currentRow = row; }); return row; }); tableView.setOnMouseClicked(me -> { if (contextMenu.isShowing()) contextMenu.hide(); boolean b = (currentRow == null || currentRow.isEmpty()); if (me.getButton() == MouseButton.PRIMARY && me.getClickCount() == 2 && !b) { edit(0); } else if (me.getButton() == MouseButton.SECONDARY) { miEdit.setDisable(b); miView.setDisable(b); miDel.setDisable(b); contextMenu.show((Node)me.getSource(), me.getScreenX(), me.getScreenY()); } }); tableView.setOnKeyPressed(ke -> { if (ke.getCode() == KeyCode.ENTER) edit(0); //else if (ke.getCode() == KeyCode.F3) { int index = tableView.getSelectionModel().getSelectedIndex(); if (index >= 0) System.out.println(sortedList.get(index).toString()); } else if (ke.getCode() == KeyCode.F5) view(); else if (ke.getCode() == KeyCode.INSERT) edit(1); else if (ke.getCode() == KeyCode.DELETE) delete(); }); tableView.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> { int index = tableView.getSelectionModel().getSelectedIndex(); lsb1.setText(index < 0 ? "" : (index + 1) + " / "+sortedList.size()); }); tableView.widthProperty().addListener((v, o, n) -> { Pane header = (Pane)tableView.lookup("TableHeaderRow"); if (header != null && header.isVisible()) { header.setMaxHeight(0); header.setMinHeight(0); header.setPrefHeight(0); header.setVisible(false); header.setManaged(false); } }); tableView.getSelectionModel().selectFirst(); } Image crImg(byte[] bytes) { if (bytes != null && bytes.length != 0) { Image img = new Image(new ByteArrayInputStream(bytes)); if (img.getWidth() > 0 && img.getHeight() > 0) return img; } return null; } Node crPict(Image image, int vr) { int size = (vr == 0 ? 50 : 200); if (image == null) { Canvas canvas = new Canvas(size, size); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.setFill(Color.LIGHTGREY); gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight()); double scale = size / 10; gc.scale(scale, scale); gc.setFill(Color.GRAY); gc.appendSVGPath("M3 3A1 1 0 007 3A1 1 0 003 3"); gc.appendSVGPath("M1 9A2 4 0 013 5A4 6 0 007 5A2 4 0 019 9Z"); gc.fill(); return canvas; } ImageView imageView = new ImageView(image); imageView.setPreserveRatio(true); imageView.setFitHeight(Math.min(imageView.getImage().getHeight(), size)); imageView.setFitWidth(Math.min(imageView.getImage().getWidth(), size)); StackPane stackPane = new StackPane(imageView); stackPane.setMinSize(size, size); return stackPane; } void edit(int vr) { int index = tableView.getSelectionModel().getSelectedIndex(), dindex = -1; VCard vc; if (vr > 0 || index >= 0) { if (vr > 0) vc = new VCard(); else { dindex = sortedList.getSourceIndex(index); vc = data.get(dindex); } Dialog<ButtonType> dialog = new Dialog<>(); dialog.initOwner(stage); dialog.initStyle(StageStyle.UTILITY); dialog.setTitle(vr > 0 ? "New" : vc.fname); dialog.setHeaderText(null); ObservableList<String> olfname = FXCollections.observableArrayList(); ComboBox<String> cbfname = new ComboBox<>(olfname); cbfname.setPrefWidth(200); cbfname.setValue(vc.fname); cbfname.setEditable(true); LocalDate localDate = (vc.bday == null ? null : vc.bday.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); DatePicker dpbday = new DatePicker(localDate); dpbday.setPrefWidth(200); dpbday.focusedProperty().addListener((v, o, n) -> { if (!n) dpbday.getOnShowing().handle(new Event(Event.ANY)); }); dpbday.setOnShowing(e -> { try { dpbday.setValue(dpbday.getConverter().fromString(dpbday.getEditor().getText())); } catch (Exception ex) { dpbday.setValue(null); dpbday.getEditor().setText(""); } }); TextField tforg = new TextField(vc.org); TextField tftitle = new TextField(vc.title); Platform.runLater(() -> cbfname.requestFocus()); GridPane gp1 = new GridPane(); gp1.setHgap(20); gp1.setVgap(6); GridPane gp2 = new GridPane(); gp2.setHgap(10); gp2.setVgap(6); Label l1, l2, l3, l4; gp2.addRow(0, l1 = new Label("Full name:"), cbfname); GridPane.setHalignment(l1, HPos.RIGHT); int r = 1; String[] t = { "Family name", "Given name", "Additional name", "Honorific prefixe", "Honorific suffixe" }; if (vc.name == null || vc.name.length != t.length) vc.name = new String[t.length]; TextField[] tf = new TextField[t.length]; for (int i = 0; i < t.length; i++) { Label l = new Label(t[i] + ":"); gp2.addRow(r++, l, tf[i] = new TextField(vc.name[i])); GridPane.setHalignment(l, HPos.RIGHT); } gp2.addRow(r++, l2 = new Label("Date of birth:"), dpbday); GridPane.setHalignment(l2, HPos.RIGHT); gp2.addRow(r++, l3 = new Label("Organization:"), tforg); GridPane.setHalignment(l3, HPos.RIGHT); gp2.addRow(r, l4 = new Label("Job title:"), tftitle); GridPane.setHalignment(l4, HPos.RIGHT); cbfname.setOnShowing(e -> { cbfname.getItems().clear(); boolean b1 = !tf[0].getText().isBlank(), b2 = !tf[1].getText().isBlank(); if (b1) cbfname.getItems().addAll(tf[0].getText()); if (b2) cbfname.getItems().addAll(tf[1].getText()); if (b1 && b2) cbfname.getItems().addAll(tf[0].getText() + " " + tf[1].getText(), tf[1].getText() + " " + tf[0].getText()); }); gp1.add(gp2, 0, 0); gp2.setAlignment(Pos.TOP_RIGHT); ObservableList<VCard.Itm> oltel = FXCollections.observableArrayList(vc.tel); int[] aiptel = { vc.ptel }; gp1.add(listEd(new String[] { "Phone" }, "Phone numbers", oltel, aiptel, 0, dialog), 0, 1); ObservableList<VCard.Itm> olemail = FXCollections.observableArrayList(vc.email); int[] aipemail = { vc.pemail }; gp1.add(listEd(new String[] { "E-mail" }, "E-mail addresses", olemail, aipemail, 1, dialog), 0, 2); ObservableList<VCard.Itm> olurl = FXCollections.observableArrayList(vc.url); int[] aipurl = { vc.purl }; gp1.add(listEd(new String[] { "URL" }, "URL", olurl, aipurl, 1, dialog), 1, 2); Object[] obj = { vc.photo, VCard.imgf[vc.itphoto < 0 || vc.itphoto >= VCard.imgf.length ? 0 : vc.itphoto].toLowerCase() }; gp1.add(imgEd(obj, dialog), 1, 0); ObservableList<VCard.Itm> oladr = FXCollections.observableArrayList(vc.adr); int[] aipadr = { vc.padr }; String[] m = { "Post office box", "Extended address (suite number)", "Street address", "Locality (city)", "Region (state or province)", "Postal code", "Country" }; gp1.add(listEd(m, "Physical address", oladr, aipadr, 2, dialog), 1, 1); TextArea taNote = new TextArea(vc.note); taNote.setPrefRowCount(2); taNote.setPrefWidth(250); HBox hb = new HBox(10, new Label("Note:"), taNote); hb.setAlignment(Pos.CENTER_RIGHT); gp1.add(hb, 0, 3); if (vc.rev != null) { TextField tfd = new TextField(DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(vc.rev)); tfd.setPrefWidth(300); tfd.setEditable(false); tfd.setFocusTraversable(false); VBox vb = new VBox(3, new Label("vCard last updated:"), tfd); vb.setAlignment(Pos.TOP_LEFT); gp1.add(vb, 1, 3); } dialog.getDialogPane().setContent(gp1); dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); Optional<ButtonType> result = dialog.showAndWait(); if(result.isPresent() && result.get() == ButtonType.OK) { bprModified.set(true); vc.fname = cbfname.getValue(); for (int i = 0; i < t.length; i++) vc.name[i] = (tf[i].getText() == null ? "" : tf[i].getText()); vc.tel.clear(); vc.tel.addAll(oltel); vc.email.clear(); vc.email.addAll(olemail); vc.url.clear(); vc.url.addAll(olurl); vc.adr.clear(); vc.adr.addAll(oladr); vc.ptel = aiptel[0]; vc.pemail = aipemail[0]; vc.purl = aipurl[0]; vc.padr = aipadr[0]; vc.bday = (dpbday.getValue() == null ? null : Date.from(dpbday.getValue().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant())); vc.org = tforg.getText(); vc.title = tftitle.getText(); vc.note = taNote.getText(); vc.photo = (byte[]) obj[0]; int i = (obj[1] == null ? -1 : Arrays.asList(VCard.imgf).indexOf(((String) obj[1]).toUpperCase())); vc.itphoto = (i < 0 ? 0 : i); vc.obj = null; vc.rev = new Date(); if (vr > 0) data.add(vc); else data.set(dindex, vc); index = sortedList.indexOf(vc); tableView.getSelectionModel().select(index); tableView.scrollTo(index); tableView.refresh(); } } } void view() { int index = tableView.getSelectionModel().getSelectedIndex(); if (index >= 0) { VCard vc = sortedList.get(index); Dialog<ButtonType> dialog = new Dialog<>(); dialog.initOwner(stage); dialog.initStyle(StageStyle.UTILITY); dialog.setResizable(true); dialog.setTitle(vc.fname); dialog.setHeaderText(null); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { vc.write(out, charset, csSpecify); } catch (Exception ex) { ex.printStackTrace(); } TextArea ta = new TextArea(new String(out.toByteArray(), charset)); VBox.setVgrow(ta, Priority.ALWAYS); ta.setEditable(false); ta.setFont(Font.font("Monospaced", Font.getDefault().getSize() + 2)); Button bt = new Button("Save"); bt.setOnAction(ae -> { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save (" + charset.name()+")"); fileChooser.setInitialDirectory(appRootPath.isDirectory() ? appRootPath : null); fileChooser.setInitialFileName("vCard.vcf"); File saveFile = fileChooser.showSaveDialog(dialog.getDialogPane().getScene().getWindow()); if (saveFile != null) { appRootPath = saveFile.getParentFile(); try { Files.write(saveFile.toPath(), out.toByteArray()); } catch (Exception ex) { alert(Alert.AlertType.ERROR, ex.toString(), dialog.getDialogPane().getScene().getWindow(), ButtonType.OK); } } }); HBox hb = new HBox(bt); hb.setPadding(new Insets(8)); VBox vb = new VBox(hb, ta); vb.setPadding(Insets.EMPTY); dialog.getDialogPane().setContent(vb); dialog.getDialogPane().setPrefSize(800, 600); dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK); Platform.runLater(() -> ta.requestFocus()); dialog.showAndWait(); } } void delete() { int index = tableView.getSelectionModel().getSelectedIndex(); if (index >= 0) { data.remove(sortedList.getSourceIndex(index)); bprModified.set(true); } } Pane imgEd(Object[] obj, Dialog<ButtonType> dlg) { Button bt1 = new Button("Load"); bt1.setMinWidth(70); Button bt2 = new Button("Remove"); bt2.setMinWidth(70); Button bt3 = new Button("Extract"); bt3.setMinWidth(70); NumberFormat nf = NumberFormat.getIntegerInstance(); Label lpi = new Label(); Image[] img = { crImg((byte[]) obj[0]) }; HBox hb = new HBox(6, bt1, bt2, bt3); hb.setAlignment(Pos.CENTER); VBox vb = new VBox(6, new Region(), lpi, hb); vb.setAlignment(Pos.CENTER); Runnable rn = () -> { vb.getChildren().set(0, crPict(img[0], 1)); lpi.setText(img[0] == null ? "" : (String) obj[1] + " / " + nf.format(img[0].getWidth()) + " x " + nf.format(img[0].getHeight()) + " / " + nf.format(((byte[]) obj[0]).length) + " Bt"); }; rn.run(); bt1.setOnAction(e -> { FileChooser fileChooser = new FileChooser(); fileChooser.getExtensionFilters().addAll(efImgs, efAll); fileChooser.setInitialDirectory(appRootPath.isDirectory() ? appRootPath : null); File openFile = fileChooser.showOpenDialog(dlg.getDialogPane().getScene().getWindow()); if (openFile != null) { appRootPath = openFile.getParentFile(); String name = openFile.getName(); int i = name.lastIndexOf('.'); String ext = (i < 0 ? "" : name.substring(i + 1).toLowerCase()), msg = null; if (i < 0 || efImgs.getExtensions().indexOf("*." + ext) < 0) msg = "Unknown format"; else if (openFile.length() > 100_000L) msg = "File is too large"; else { try { byte[] bytes = Files.readAllBytes(openFile.toPath()); Image im = crImg(bytes); if (im == null) msg = "No image file"; else { if (ext.equals("jpg")) ext = "jpeg"; obj[0] = bytes; obj[1] = ext; img[0] = im; rn.run(); } } catch (Exception ex) { msg = ex.toString(); } } if (msg != null) alert(Alert.AlertType.ERROR, msg, dlg.getDialogPane().getScene().getWindow(), ButtonType.OK); } }); bt2.setOnAction(e -> { if (obj[0] != null) { obj[0] = obj[1] = img[0] = null; rn.run(); } }); bt3.setOnAction(e -> { if (obj[0] != null) { FileChooser fileChooser = new FileChooser(); fileChooser.setInitialDirectory(appRootPath.isDirectory() ? appRootPath : null); String ext = ((String) obj[1]); fileChooser.setInitialFileName("image." + (ext.equals("jpeg") ? "jpg" : ext)); File saveFile = fileChooser.showSaveDialog(dlg.getDialogPane().getScene().getWindow()); if (saveFile != null) { appRootPath = saveFile.getParentFile(); try { Files.write(saveFile.toPath(), (byte[]) obj[0]); } catch (Exception ex) { alert(Alert.AlertType.ERROR, ex.toString(), dlg.getDialogPane().getScene().getWindow(), ButtonType.OK); } } } }); return vb; } ButtonType alert(Alert.AlertType type, String cont, Window window, ButtonType... bt) { Alert alert = new Alert(type, cont, bt); alert.initOwner(window); alert.initStyle(StageStyle.UTILITY); alert.setHeaderText(null); Optional<ButtonType> result = alert.showAndWait(); return (result.isPresent() ? result.get() : null); } Node listEd(String[] tl1, String tl2, ObservableList<VCard.Itm> olist, int[] p, int vr, Dialog<ButtonType> dlg) { Button bt1 = new Button("Add"); bt1.setMinWidth(70); Button bt2 = new Button("Edit"); bt2.setMinWidth(70); Button bt3 = new Button("Remove"); bt3.setMinWidth(70); Button bt4 = new Button(null, shp(12, 12, "M0 7L6 0L12 7H8V12H4V7Z")); bt4.setMinWidth(32); Button bt5 = new Button(null, shp(12, 12, "M0 5L6 12L12 5H8V0H4V5Z")); bt5.setMinWidth(32); Font fontBold = Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize() + 1); Function<String[], String> jn = (t) -> { String s = Arrays.stream(t).reduce("", (x, y) -> x + (y == null || y.isBlank() ? "" : ", " + y.replace('\n', ' '))); return (s.length() < 2 ? "" : s.substring(2)); }; ListView<VCard.Itm> listView = new ListView<>(olist); listView.setCellFactory(c -> { ListCell<VCard.Itm> cell = new ListCell<>() { @Override public void updateItem(VCard.Itm item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) setGraphic(null); else { Label l4 = new Label(item.note.replace('\n', ' ')); Label l3 = new Label(p[0] == getIndex() ? "p" : ""); if (vr == 2) { Label l1 = new Label(jn.apply((String[])item.value)); l1.setFont(fontBold); l1.setMinWidth(140); l1.setMaxWidth(140); Label l2 = new Label(VCard.tadr[(item.idl < 0 || item.idl >= VCard.tadr.length ? 0 : item.idl)]); l2.setMinWidth(40); l2.setMaxWidth(40); setGraphic(new VBox(new HBox(10, l1, l2, l3), l4)); } else if (vr == 0) { Label l1 = new Label((String)item.value); l1.setFont(fontBold); l1.setMinWidth(140); l1.setMaxWidth(140); Label l2 = new Label(VCard.ttel[(item.idl < 0 || item.idl >= VCard.ttel.length ? 0 : item.idl)]); l2.setMinWidth(40); l2.setMaxWidth(40); setGraphic(new VBox(new HBox(10, l1, l2, l3), l4)); } else { // vr == 1 Label l1 = new Label((String)item.value); l1.setFont(fontBold); l1.setMinWidth(190); l1.setMaxWidth(190); setGraphic(new VBox(new HBox(10, l1, l3), l4)); } } } }; cell.setOnMouseClicked(me -> { if (!cell.isEmpty() && me.getButton() == MouseButton.PRIMARY && me.getClickCount() == 2) bt2.fire(); }); return cell; }); EventHandler<ActionEvent> ae1 = e -> { int index = listView.getSelectionModel().getSelectedIndex(); if (e.getTarget() == bt1 || index >= 0) { if (e.getTarget() == bt1) index = -1; int ind; boolean b; String note; String[] m; if (index < 0) { Arrays.fill(m = new String[tl1.length], ""); ind = 0; note = ""; b = false; } else { if (vr == 2) { m = Arrays.copyOf((String[])olist.get(index).value, tl1.length); } else { m = new String[] { (String)olist.get(index).value }; } ind = olist.get(index).idl; note = olist.get(index).note; b = (index == p[0]); } Dialog<ButtonType> dialog = new Dialog<>(); dialog.initOwner(dlg.getDialogPane().getScene().getWindow()); dialog.initStyle(StageStyle.UTILITY); dialog.setTitle(index < 0 ? "New" : jn.apply(m)); dialog.setHeaderText(null); GridPane gridPane = new GridPane(); gridPane.setHgap(10); gridPane.setVgap(6); TextInputControl[] tf = new TextInputControl[m.length]; int r = 0; for (int i = 0; i < m.length; i++) { Label l = new Label(tl1[i] + ":"); if (i == 2) { tf[i] = new TextArea(m[i]); ((TextArea)tf[i]).setPrefRowCount(2); } else tf[i] = new TextField(m[i]); gridPane.addRow(r++, l, tf[i]); GridPane.setHalignment(l, HPos.RIGHT); tf[i].setPrefWidth(200); } String[] t = (vr == 0 ? VCard.ttel : VCard.tadr); ComboBox<String> comboBox = new ComboBox<>(FXCollections.observableArrayList(t)); comboBox.setPrefWidth(200); comboBox.setValue(t[(ind < 0 || ind >= t.length ? 0 : ind)]); Label l1, l2, l3; if (vr != 1) { gridPane.addRow(r++, l1 = new Label("Type:"), comboBox); GridPane.setHalignment(l1, HPos.RIGHT); } CheckBox checkBox = new CheckBox(); checkBox.setSelected(b); gridPane.addRow(r++, l2 = new Label("Preferred:"), checkBox); GridPane.setHalignment(l2, HPos.RIGHT); TextArea ta = new TextArea(note); ta.setPrefWidth(200); ta.setPrefRowCount(2); gridPane.addRow(r, l3 = new Label("Note:"), ta); GridPane.setHalignment(l3, HPos.RIGHT); dialog.getDialogPane().setContent(gridPane); Platform.runLater(() -> tf[0].requestFocus()); dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); Optional<ButtonType> result = dialog.showAndWait(); if(result.isPresent() && result.get() == ButtonType.OK) { for (int i = 0; i < m.length; i++) m[i] = (tf[i].getText() == null ? "" : tf[i].getText()); if (index < 0) { olist.add(null); index = olist.size() - 1; } int intg = (vr == 1 ? -1 : comboBox.getItems().indexOf(comboBox.getValue())); VCard.Itm itm; if (vr == 2) itm = new VCard.Itm("", intg, m, ta.getText()); else itm = new VCard.Itm("", intg, tf[0].getText(), ta.getText()); olist.set(index, itm); if (checkBox.isSelected()) p[0] = index; else if (index == p[0]) p[0] = -1; listView.getSelectionModel().select(index); listView.scrollTo(index); listView.refresh(); } } }; bt1.setOnAction(ae1); bt2.setOnAction(ae1); bt3.setOnAction(e -> { int index = listView.getSelectionModel().getSelectedIndex(); if (index >= 0) { olist.remove(index); if (index == p[0]) p[0] = -1; else if (index < p[0]) p[0]--; } }); EventHandler<ActionEvent> ae2 = e -> { int index = listView.getSelectionModel().getSelectedIndex(); if ((e.getTarget() == bt4 && index > 0 && olist.size() > 1) || (e.getTarget() != bt4 && index >= 0 && index < olist.size() - 1)) { int newIndex = (e.getTarget() == bt4 ? index - 1 : index + 1); olist.set(index, olist.set(newIndex, olist.get(index))); if (newIndex == p[0]) p[0] = index; else if (index == p[0]) p[0] = newIndex; listView.getSelectionModel().select(newIndex); listView.scrollTo(newIndex); } }; bt4.setOnAction(ae2); bt5.setOnAction(ae2); listView.setOnKeyPressed(ke -> { if (ke.getCode() == KeyCode.ENTER) bt2.fire(); else if (ke.getCode() == KeyCode.ESCAPE) Event.fireEvent(dlg.getDialogPane(), ke); }); listView.setPrefSize(240, 120); listView.getSelectionModel().selectFirst(); return new VBox(3, new Label(tl2 + ":"), new HBox(10, listView, new VBox(6, bt1, bt2, bt3, new HBox(6, bt4, bt5)))); } Canvas shp(int w, int h, String p) { Canvas canvas = new Canvas(w, h); GraphicsContext gc = canvas.getGraphicsContext2D(); gc.setFill(Color.grayRgb(87)); gc.appendSVGPath(p); gc.fill(); return canvas; } ObservableList<String> getOblCharset() { if (oblCharset == null) { oblCharset = FXCollections.observableArrayList(Charset.defaultCharset().name(), StandardCharsets.UTF_8.name()); oblCharset.addAll(Charset.availableCharsets().keySet()); } return oblCharset; } void options() { Dialog<ButtonType> dialog = new Dialog<>(); dialog.initOwner(stage); dialog.initStyle(StageStyle.UTILITY); dialog.setTitle("Options"); dialog.setHeaderText(null); ComboBox<String> comboBox = new ComboBox<>(getOblCharset()); comboBox.setPrefWidth(150); comboBox.setValue(charset.name()); ObservableList<String> oblCharset2 = FXCollections.observableArrayList("Charset + QP", "Specified", "Not specified"); ComboBox<String> comboBox2 = new ComboBox<>(oblCharset2); comboBox2.setPrefWidth(150); comboBox2.setValue(oblCharset2.get(csSpecify)); GridPane gridPane = new GridPane(); gridPane.setHgap(10); gridPane.setVgap(6); Label l1, l2; gridPane.addRow(0, l1 = new Label("Text charset:"), comboBox); GridPane.setHalignment(l1, HPos.RIGHT); gridPane.addRow(1, l2 = new Label("Specify charset:"), comboBox2); GridPane.setHalignment(l2, HPos.RIGHT); dialog.getDialogPane().setContent(gridPane); dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); Platform.runLater(() -> comboBox.requestFocus()); Optional<ButtonType> result = dialog.showAndWait(); if(result.isPresent() && result.get() == ButtonType.OK) { try { charset = Charset.forName(comboBox.getValue()); } catch (Exception e) { } csSpecify = oblCharset2.indexOf(comboBox2.getValue()); lsb2.setText(charset.name() + (csSpecify == 0 ? "" : " / " + (csSpecify == 1 ? "sp" : "nsp"))); } } Node search() { TextField tf = new TextField(); tf.setPrefWidth(200); Button bt1 = new Button(null, shp(8, 4, "M0 0 H8 L4 4 Z")), bt2 = new Button(null, shp(8, 4, "M0 4 H8 L4 0 Z")); bt1.setDisable(true); bt2.setDisable(true); tf.setOnKeyPressed(ke -> { if (ke.getCode() == KeyCode.ENTER) (ke.isShiftDown() ? bt2 : bt1).fire(); }); tf.textProperty().addListener((v, o, n) -> { boolean b = tf.getText().isBlank(); bt1.setDisable(b); bt2.setDisable(b); }); EventHandler<ActionEvent> ae = e -> { int index = tableView.getSelectionModel().getSelectedIndex(); if (index >= 0) { int ind = index; VCard vc = null; do { index += (e.getTarget() == bt1 ? 1 : -1); if (index < 0) index = sortedList.size() - 1; else if (index >= sortedList.size()) index = 0; if (ind == index) break; vc = sortedList.get(index); } while (!vc.contains(tf.getText())); if (ind != index) { tableView.getSelectionModel().select(index); tableView.scrollTo(index); } } }; bt1.setOnAction(ae); bt2.setOnAction(ae); HBox hb = new HBox(8, new Label("Search:"), new HBox(tf, bt1, bt2)); hb.setAlignment(Pos.CENTER); hb.setPadding(new Insets(4, 15, 4, 0)); return hb; } }