diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index dd402d03..b5ed64d7 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -54,6 +54,7 @@ import ctbrec.sites.stripchat.Stripchat; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.news.NewsTab; import ctbrec.ui.settings.SettingsTab; +import ctbrec.ui.settings.SettingsTab2; import ctbrec.ui.tabs.DonateTabFx; import ctbrec.ui.tabs.HelpTab; import ctbrec.ui.tabs.RecordedModelsTab; @@ -190,6 +191,7 @@ public class CamrecApplication extends Application { tabPane.getTabs().add(recordingsTab); settingsTab = new SettingsTab(sites, recorder); tabPane.getTabs().add(settingsTab); + tabPane.getTabs().add(new SettingsTab2(sites, recorder)); tabPane.getTabs().add(new NewsTab()); tabPane.getTabs().add(new DonateTabFx()); tabPane.getTabs().add(new HelpTab()); @@ -205,6 +207,7 @@ public class CamrecApplication extends Application { primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css"); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/api/Preferences.css"); primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue()); primaryStage.getScene().heightProperty() .addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue()); diff --git a/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/AutoFillTextField.java similarity index 66% rename from client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java rename to client/src/main/java/ctbrec/ui/controls/autocomplete/AutoFillTextField.java index ca772778..6b12b354 100644 --- a/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/AutoFillTextField.java @@ -1,7 +1,8 @@ -package ctbrec.ui.controls; +package ctbrec.ui.controls.autocomplete; -import javafx.collections.ObservableList; +import java.util.Optional; + import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.control.TextField; @@ -10,12 +11,12 @@ import javafx.scene.input.KeyEvent; public class AutoFillTextField extends TextField { - private ObservableList suggestions; private EventHandler handler; + private Suggester suggester; - public AutoFillTextField(ObservableList suggestions) { - this.suggestions = suggestions; - addEventHandler(KeyEvent.KEY_RELEASED, (evt) -> { + public AutoFillTextField(Suggester suggester) { + this.suggester = suggester; + addEventHandler(KeyEvent.KEY_RELEASED, evt -> { if (evt.getCode().isLetterKey() || evt.getCode().isDigitKey()) { autocomplete(false); } else if (evt.getCode() == KeyCode.ENTER) { @@ -38,16 +39,19 @@ public class AutoFillTextField extends TextField { if(oldtext.isEmpty()) { return; } - for (String sug : suggestions) { - boolean startsWith = sug.toLowerCase().startsWith(oldtext.toLowerCase()); - boolean textMatch = fulltextSearch && sug.toLowerCase().contains(oldtext.toLowerCase()); - if(startsWith || textMatch) { - setText(sug); - int pos = oldtext.length(); - positionCaret(pos); - selectRange(pos, sug.length()); - break; - } + + Optional match = null; + if(fulltextSearch) { + match = suggester.fulltext(oldtext); + } else { + match = suggester.startsWith(oldtext); + } + + if(match.isPresent()) { + setText(match.get()); + int pos = oldtext.length(); + positionCaret(pos); + selectRange(pos, match.get().length()); } } diff --git a/client/src/main/java/ctbrec/ui/controls/autocomplete/ObservableListSuggester.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/ObservableListSuggester.java new file mode 100644 index 00000000..a4970b32 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/ObservableListSuggester.java @@ -0,0 +1,37 @@ +package ctbrec.ui.controls.autocomplete; + +import java.util.Optional; + +import javafx.collections.ObservableList; + +public class ObservableListSuggester implements Suggester { + + private ObservableList suggestions; + + public ObservableListSuggester(ObservableList suggestions) { + this.suggestions = suggestions; + } + + @Override + public Optional startsWith(String search) { + for (Object sug : suggestions) { + boolean startsWith = sug.toString().toLowerCase().startsWith(search.toLowerCase()); + if (startsWith) { + return Optional.of(sug.toString()); + } + } + return Optional.empty(); + } + + @Override + public Optional fulltext(String search) { + for (Object sug : suggestions) { + boolean startsWith = sug.toString().toLowerCase().contains(search.toLowerCase()); + if (startsWith) { + return Optional.of(sug.toString()); + } + } + return Optional.empty(); + } + +} diff --git a/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggester.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggester.java new file mode 100644 index 00000000..f3e1e8c2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggester.java @@ -0,0 +1,9 @@ +package ctbrec.ui.controls.autocomplete; + +import java.util.Optional; + +public interface Suggester { + + public Optional startsWith(String search); + public Optional fulltext(String search); +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggestion.java b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggestion.java new file mode 100644 index 00000000..82378795 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/autocomplete/Suggestion.java @@ -0,0 +1,5 @@ +package ctbrec.ui.controls.autocomplete; + +public class Suggestion { + +} diff --git a/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java b/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java new file mode 100644 index 00000000..d0f2c3e9 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/CtbrecPreferencesStorage.java @@ -0,0 +1,106 @@ +package ctbrec.ui.settings; + +import java.io.IOException; +import java.lang.reflect.Field; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.ui.settings.api.Preferences; +import ctbrec.ui.settings.api.PreferencesStorage; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; + +public class CtbrecPreferencesStorage implements PreferencesStorage { + + private static final Logger LOG = LoggerFactory.getLogger(CtbrecPreferencesStorage.class); + public static final String PATTERN_NOT_A_DIGIT = "[^\\d]"; + public static final String COULDNT_SAVE_MSG = "Couldn't save config setting"; + + private Config config; + private Settings settings; + + public CtbrecPreferencesStorage(Config config) { + this.config = config; + this.settings = config.getSettings(); + } + + @Override + public void save(Preferences preferences) throws IOException { + config.save(); + } + + @Override + public void load(Preferences preferences) { + throw new RuntimeException("not implemented"); + } + + @Override + public Node createGui(String key) throws Exception { + Field field = Settings.class.getField(key); + Class t = field.getType(); + Object value = field.get(settings); + if (t == String.class) { + return createStringProperty(key, (String) value); + } else if (t == int.class || t == Integer.class) { + return createIntegerProperty(key, (Integer) value); + } else if (t == boolean.class || t == Boolean.class) { + return createBooleanProperty(key, (Boolean) value); + } else { + return new Label("Unsupported Type for key " + key + ": " + t); + } + } + + private Node createStringProperty(String fieldName, String value) { + TextField ctrl = new TextField(value); + ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + Field field = Settings.class.getField(fieldName); + field.set(settings, newV); + config.save(); + })); + return ctrl; + } + + private Node createIntegerProperty(String fieldName, Integer value) { + TextField ctrl = new TextField(value.toString()); + ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + if (!newV.matches("\\d*")) { + ctrl.setText(newV.replaceAll(PATTERN_NOT_A_DIGIT, "")); + } + if (!ctrl.getText().isEmpty()) { + Field field = Settings.class.getField(fieldName); + field.set(settings, Integer.parseInt(ctrl.getText())); + config.save(); + } + })); + return ctrl; + } + + private Node createBooleanProperty(String fieldName, Boolean value) { + CheckBox ctrl = new CheckBox(); + ctrl.setSelected(value); + ctrl.selectedProperty().addListener((obs, oldV, newV) -> saveValue(() -> { + Field field = Settings.class.getField(fieldName); + field.set(settings, newV); + config.save(); + })); + return ctrl; + } + + private void saveValue(Exec exe) { + try { + exe.run(); + } catch (Exception e) { + LOG.error(COULDNT_SAVE_MSG, e); + } + } + + @FunctionalInterface + private interface Exec { + public void run() throws Exception; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/PostProcessingSettingsDialog.java b/client/src/main/java/ctbrec/ui/settings/PostProcessingSettingsDialog.java new file mode 100644 index 00000000..a008fb94 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/PostProcessingSettingsDialog.java @@ -0,0 +1,101 @@ +package ctbrec.ui.settings; + +import java.io.IOException; +import java.io.InputStream; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.stage.Modality; +import javafx.stage.Stage; + +public class PostProcessingSettingsDialog extends Dialog { + + private Scene parent; + private Config config; + private Settings settings; + + private TextField playerParams; + private TextField maxResolution; + + public PostProcessingSettingsDialog(Scene parent, Config config) { + this.parent = parent; + this.config = config; + this.settings = config.getSettings(); + + initGui(); + } + + private void initGui() { + setTitle("Player Settings"); + setHeaderText("Player Settings"); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + initModality(Modality.APPLICATION_MODAL); + setResizable(true); + InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); + Stage stage = (Stage) getDialogPane().getScene().getWindow(); + stage.getIcons().add(new Image(icon)); + if (parent != null) { + stage.getScene().getStylesheets().addAll(parent.getStylesheets()); + } + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + grid.add(new Label("Start parameters"), 0, 0); + playerParams = new TextField(settings.mediaPlayerParams); + grid.add(playerParams, 1, 0); + getDialogPane().setContent(grid); + GridPane.setFillWidth(playerParams, true); + GridPane.setHgrow(playerParams, Priority.ALWAYS); + + Label l = new Label("Maximum resolution (0 = unlimited)"); + grid.add(l, 0, 1); + maxResolution = new TextField(Integer.toString(settings.maximumResolutionPlayer)); + Tooltip tt = new Tooltip("video height, e.g. 720 or 1080"); + l.setTooltip(tt); + maxResolution.setTooltip(tt); + grid.add(maxResolution, 1, 1); + getDialogPane().setContent(grid); + GridPane.setFillWidth(maxResolution, true); + GridPane.setHgrow(maxResolution, Priority.ALWAYS); + + Platform.runLater(playerParams::requestFocus); + + setResultConverter(dialogButton -> { + try { + if (dialogButton == ButtonType.OK) { + saveSettings(); + } + return null; + } catch (IOException e) { + return e; + } + }); + } + + public void saveSettings() throws IOException { + settings.mediaPlayerParams = playerParams.getText(); + String res = maxResolution.getText(); + if (res.matches("\\d+")) { + int newRes = Integer.parseInt(maxResolution.getText()); + if (newRes != Config.getInstance().getSettings().maximumResolutionPlayer) { + settings.maximumResolutionPlayer = newRes; + } + } + config.save(); + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab2.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab2.java new file mode 100644 index 00000000..67bf5245 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab2.java @@ -0,0 +1,66 @@ +package ctbrec.ui.settings; + +import java.util.ArrayList; +import java.util.List; + +import ctbrec.Config; +import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.settings.api.Category; +import ctbrec.ui.settings.api.Group; +import ctbrec.ui.settings.api.Preferences; +import ctbrec.ui.settings.api.Setting; +import ctbrec.ui.tabs.TabSelectionListener; +import javafx.scene.control.Tab; + +public class SettingsTab2 extends Tab implements TabSelectionListener { + + private List sites; + private Recorder recorder; + + public SettingsTab2(List sites, Recorder recorder) { + this.sites = sites; + this.recorder = recorder; + setText("Settings"); + createGui(); + setClosable(false); + } + + private void createGui() { + List siteCategories = new ArrayList<>(); + for (Site site : sites) { + siteCategories.add(Category.of(site.getName(), SiteUiFactory.getUi(site).getConfigUI().createConfigPanel())); + } + + Preferences prefs = Preferences.of(new CtbrecPreferencesStorage(Config.getInstance()), + Category.of("General", + Group.of("General", + Setting.of("Player", "mediaPlayer"), + Setting.of("Start parameters", "mediaPlayerParams"), + Setting.of("Maximum resolution (0 = unlimited)", "maximumResolutionPlayer", "video height, e.g. 720 or 1080")), + Group.of("Player", + Setting.of("Player", "mediaPlayer"), + Setting.of("Start parameters", "mediaPlayerParams"), + Setting.of("Maximum resolution (0 = unlimited)", "maximumResolutionPlayer", "video height, e.g. 720 or 1080"), + Setting.of("Show \"Player Starting\" Message", "showPlayerStarting")) + ), + Category.of("Sites", siteCategories.toArray(new Category[0])) + ); + setContent(prefs.getView()); + } + + @Override + public void selected() { + // TODO Auto-generated method stub + + } + + @Override + public void deselected() { + // TODO Auto-generated method stub + + } + + +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Category.java b/client/src/main/java/ctbrec/ui/settings/api/Category.java new file mode 100644 index 00000000..df43a3b7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Category.java @@ -0,0 +1,153 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import java.util.function.Supplier; + +import ctbrec.StringUtil; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Pane; + +public class Category { + + protected String name; + protected Group[] groups; + protected Category[] subCategories; + protected Node gui; + + private Category(String name, Group... groups) { + this.name = name; + this.groups = groups; + } + + public Category(String name, Category[] subCategories) { + this.name = name; + this.subCategories = subCategories; + groups = new Group[0]; + } + + public Category(String name, Node gui) { + this.name = name; + this.gui = gui; + groups = new Group[0]; + } + + public static Category of(String name, Setting... settings) { + return new Category(name, Group.of(settings)); + } + + public static Category of(String name, Group... groups) { + return new Category(name, groups); + } + + public static Category of(String name, Category... subCategories) { + return new Category(name, subCategories); + } + + public static Category of(String name, Node gui) { + return new Category(name, gui); + } + + String getName() { + return name; + } + + Group[] getGroups() { + return groups; + } + + Category[] getSubCategories() { + return subCategories; + } + + boolean hasGroups() { + return groups != null && groups.length > 0 && !groups[0].isDefault(); + } + + boolean hasSubCategories() { + return subCategories != null && subCategories.length > 0; + } + + Node getGuiOrElse(Supplier guiFactory) { + if (gui == null) { + gui = guiFactory.get(); + } + return gui; + } + + @Override + public String toString() { + return name; + } + + public boolean contains(String filter) { + if (StringUtil.isBlank(filter)) { + return true; + } + String q = filter.toLowerCase().trim(); + if(hasGroups() || hasSubCategories()) { + return name.toLowerCase().contains(q) + | groupsContains(q) + | subCategoriesContains(q); + } else { + return name.toLowerCase().contains(q) + | guiContains(q); + } + + } + + private boolean subCategoriesContains(String filter) { + boolean contains = false; + if (subCategories != null) { + for (Category category : subCategories) { + if (category.contains(filter)) { + contains = true; + } + } + } + return contains; + } + + private boolean groupsContains(String filter) { + boolean contains = false; + if (groups != null) { + for (Group group : groups) { + if (group.contains(filter)) { + contains = true; + } + } + } + return contains; + } + + private boolean guiContains(String filter) { + if (gui != null) { + return nodeContains(gui, filter); + } + return false; + } + + private boolean nodeContains(Node node, String filter) { + boolean contains = false; + if (node instanceof Pane) { + Pane pane = (Pane) node; + for (Node child : pane.getChildren()) { + contains |= nodeContains(child, filter); + } + } + + if (node instanceof Label) { + Label lbl = (Label) node; + contains |= lbl.getText().toLowerCase().contains(filter); + contains |= ofNullable(lbl.getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + } + if (node instanceof Control) { + contains |= ofNullable(((Control) node).getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + } + contains |= node.toString().toLowerCase().contains(filter); + return contains; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Group.java b/client/src/main/java/ctbrec/ui/settings/api/Group.java new file mode 100644 index 00000000..d3af7040 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Group.java @@ -0,0 +1,51 @@ +package ctbrec.ui.settings.api; + +import java.util.Objects; + +public class Group { + + public static final String DEFAULT = "default"; + private String name; + private Setting[] settings; + + private Group(String name, Setting...settings) { + this.name = name; + this.settings = settings; + } + + public static Group of(Setting...settings) { + return new Group(DEFAULT, settings); + } + + public static Group of(String name, Setting...settings) { + return new Group(name, settings); + } + + String getName() { + return name; + } + + Setting[] getSettings() { + return settings; + } + + boolean isDefault() { + return Objects.equals(name, DEFAULT); + } + + public boolean contains(String filter) { + return name.toLowerCase().contains(filter) | settingsContain(filter); + } + + private boolean settingsContain(String filter) { + boolean contains = false; + if (settings != null) { + for (Setting setting : settings) { + if (setting.contains(filter)) { + contains = true; + } + } + } + return contains; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/HighlightingSupport.java b/client/src/main/java/ctbrec/ui/settings/api/HighlightingSupport.java new file mode 100644 index 00000000..5776f54a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/HighlightingSupport.java @@ -0,0 +1,89 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.TextInputControl; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Pane; + +class HighlightingSupport { + + private static final String CSS_HIGHLIGHT_CLASS = "setting-highlighted"; + + private HighlightingSupport() {} + + static void highlightMatchess(Category cat, String filter) { + Node node = cat.getGuiOrElse(Label::new); + highlightMatchess(node, filter); + if(cat.hasSubCategories()) { + for (Category sub : cat.getSubCategories()) { + highlightMatchess(sub, filter); + } + } + } + + static void highlightMatchess(Node node, String filter) { + boolean contains = false; + if (node instanceof Pane) { + Pane pane = (Pane) node; + for (Node child : pane.getChildren()) { + highlightMatchess(child, filter); + } + } + + if (node instanceof Label) { + Label lbl = (Label) node; + contains |= lbl.getText().toLowerCase().contains(filter); + contains |= ofNullable(lbl.getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + contains |= labelControlContains(lbl, filter); + + if (contains) { + if (!node.getStyleClass().contains(CSS_HIGHLIGHT_CLASS)) { + node.getStyleClass().add(CSS_HIGHLIGHT_CLASS); + } + } else { + node.getStyleClass().remove(CSS_HIGHLIGHT_CLASS); + } + } + } + + private static boolean labelControlContains(Label lbl, String filter) { + boolean contains = false; + if (lbl.labelForProperty().get() != null) { + Node labeledNode = lbl.labelForProperty().get(); + contains |= labeledNode.toString().toLowerCase().contains(filter); + if (labeledNode instanceof Control) { + contains |= ofNullable(((Control) labeledNode).getTooltip()).map(Tooltip::getText).orElse("").toLowerCase().contains(filter); + } + if (labeledNode instanceof TextInputControl) { + contains |= ((TextInputControl) labeledNode).getText().toLowerCase().contains(filter); + } + } + return contains; + } + + static void removeHighlights(Category cat) { + Node node = cat.getGuiOrElse(Label::new); + removeHighlights(node); + if(cat.hasSubCategories()) { + for (Category sub : cat.getSubCategories()) { + removeHighlights(sub); + } + } + } + + static void removeHighlights(Node gui) { + if (gui != null) { + if (gui instanceof Pane) { + Pane p = (Pane) gui; + for (Node n : p.getChildren()) { + removeHighlights(n); + } + } + gui.getStyleClass().remove(CSS_HIGHLIGHT_CLASS); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Preferences.css b/client/src/main/java/ctbrec/ui/settings/api/Preferences.css new file mode 100644 index 00000000..e3d88a47 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Preferences.css @@ -0,0 +1,8 @@ +.settings-group-label { + -fx-font-size: 1.6em; +} + +.setting-highlighted { + -fx-border-color: -fx-accent; + -fx-border-width: 3; +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/settings/api/Preferences.java b/client/src/main/java/ctbrec/ui/settings/api/Preferences.java new file mode 100644 index 00000000..fe322d0e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Preferences.java @@ -0,0 +1,182 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.ui.controls.SearchBox; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +public class Preferences { + + private static final Logger LOG = LoggerFactory.getLogger(Preferences.class); + + private Category[] categories; + + private TreeView categoryTree; + + private Preferences(PreferencesStorage preferencesStorage, Category...categories) { + this.categories = categories; + for (Category category : categories) { + assignPreferencesStorage(category, preferencesStorage); + } + } + + private void assignPreferencesStorage(Category cat, PreferencesStorage preferencesStorage) { + if(cat.hasSubCategories()) { + for (Category sub : cat.getSubCategories()) { + assignPreferencesStorage(sub, preferencesStorage); + } + } else { + for (Group group : cat.getGroups()) { + for (Setting setting : group.getSettings()) { + setting.setPreferencesStorage(preferencesStorage); + } + } + } + } + + public static Preferences of(PreferencesStorage preferencesStorage, Category... categories) { + return new Preferences(preferencesStorage, categories); + } + + public void save() { + throw new RuntimeException("save not implemented"); + } + + Category[] getCategories() { + return categories; + } + + public Node getView() { + SearchBox search = new SearchBox(true); + search.textProperty().addListener(this::filterTree); + TreeItem categoryTreeItems = createCategoryTree(categories, new TreeItem<>(), null); + categoryTree = new TreeView<>(categoryTreeItems); + categoryTree.showRootProperty().set(false); + VBox leftSide = new VBox(search, categoryTree); + VBox.setVgrow(categoryTree, Priority.ALWAYS); + VBox.setMargin(search, new Insets(2)); + VBox.setMargin(categoryTree, new Insets(2)); + + BorderPane main = new BorderPane(); + main.setLeft(leftSide); + main.setCenter(new Label("Center")); + BorderPane.setMargin(leftSide, new Insets(2)); + + categoryTree.getSelectionModel().selectedItemProperty().addListener((obs, oldV, newV) -> { + if (newV != null) { + Category cat = newV.getValue(); + Node gui = cat.getGuiOrElse(() -> createGui(cat)); + BorderPane.setMargin(gui, new Insets(10)); + main.setCenter(gui); + } + }); + + return main; + } + + private void filterTree(ObservableValue obs, String oldV, String newV) { + String q = ofNullable(newV).orElse("").toLowerCase().trim(); + TreeItem filteredCategoryTree = createCategoryTree(categories, new TreeItem<>(), q); + categoryTree.setRoot(filteredCategoryTree); + expandAll(categoryTree.getRoot()); + + TreeItem parent = categoryTree.getRoot(); + while (!parent.getChildren().isEmpty()) { + parent = parent.getChildren().get(0); + categoryTree.getSelectionModel().select(parent); + } + + for (Category category : categories) { + if (q.length() > 2) { + HighlightingSupport.highlightMatchess(category, q); + } else { + HighlightingSupport.removeHighlights(category); + } + } + } + + private void expandAll(TreeItem treeItem) { + treeItem.setExpanded(true); + for (TreeItem child : treeItem.getChildren()) { + expandAll(child); + } + } + + private Node createGui(Category cat) { + try { + if (cat.hasSubCategories()) { + return new Label(cat.getName()); + } else if(cat.hasGroups()) { + return createPaneWithGroups(cat); + } else { + return createGrid(cat.getGroups()[0].getSettings()); + } + } catch(Exception e) { + LOG.error("Error creating the GUI", e); + return new Label(e.getLocalizedMessage()); + } + } + + private Node createPaneWithGroups(Category cat) throws Exception { + VBox pane = new VBox(); + for (Group grp : cat.getGroups()) { + Label groupLabel = new Label(grp.getName()); + groupLabel.getStyleClass().add("settings-group-label"); + VBox.setMargin(groupLabel, new Insets(20, 0, 10, 20)); + pane.getChildren().add(groupLabel); + Node parameterGrid = createGrid(grp.getSettings()); + pane.getChildren().add(parameterGrid); + VBox.setMargin(parameterGrid, new Insets(0, 0, 0, 40)); + } + return pane; + } + + private Node createGrid(Setting[] settings) throws Exception { + GridPane pane = new GridPane(); + int row = 0; + for (Setting setting : settings) { + Node node = setting.getGui(); + Label label = new Label(setting.getName()); + label.labelForProperty().set(node); + if (setting.getTooltip() != null) { + label.setTooltip(new Tooltip(setting.getTooltip())); + } + pane.addRow(row++, label, node); + GridPane.setMargin(node, new Insets(5, 0, 5, 10)); + GridPane.setHgrow(node, Priority.ALWAYS); + } + return pane; + } + + /** + * Creates a tree of the given categories. Filters out categories, which don't match the filter + * @param filter may be null + */ + private TreeItem createCategoryTree(Category[] categories, TreeItem parent, String filter) { + for (Category category : categories) { + TreeItem child = new TreeItem<>(category); + if (category.hasSubCategories()) { + createCategoryTree(category.getSubCategories(), child, filter); + if (!child.getChildren().isEmpty()) { + parent.getChildren().add(child); + } + } else if(category.contains(filter)) { + parent.getChildren().add(child); + } + } + return parent; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/PreferencesStorage.java b/client/src/main/java/ctbrec/ui/settings/api/PreferencesStorage.java new file mode 100644 index 00000000..b57d2ae2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/PreferencesStorage.java @@ -0,0 +1,13 @@ +package ctbrec.ui.settings.api; + +import java.io.IOException; + +import javafx.scene.Node; + +public interface PreferencesStorage { + + void save(Preferences preferences) throws IOException; + void load(Preferences preferences); + + Node createGui(String key) throws Exception; +} diff --git a/client/src/main/java/ctbrec/ui/settings/api/Setting.java b/client/src/main/java/ctbrec/ui/settings/api/Setting.java new file mode 100644 index 00000000..f04891e0 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/api/Setting.java @@ -0,0 +1,73 @@ +package ctbrec.ui.settings.api; + +import static java.util.Optional.*; + +import ctbrec.StringUtil; +import javafx.beans.property.Property; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Tooltip; + +public class Setting { + + private String name; + private String key; + private String tooltip; + private Property property; + private Node gui; + private PreferencesStorage preferencesStorage; + + private Setting(String name, String key) { + this.name = name; + this.key = key; + } + + public static Setting of(String name, String key) { + return new Setting(name, key); + } + + public static Setting of(String name, String key, String tooltip) { + Setting setting = new Setting(name, key); + setting.tooltip = tooltip; + return setting; + } + + String getName() { + return name; + } + + String getKey() { + return key; + } + + String getTooltip() { + return tooltip; + } + + @SuppressWarnings("rawtypes") + Property getProperty() { + return property; + } + + Node getGui() throws Exception { + if (gui == null) { + gui = preferencesStorage.createGui(key); + if (gui instanceof Control && StringUtil.isNotBlank(tooltip)) { + Control control = (Control) gui; + control.setTooltip(new Tooltip(tooltip)); + } + } + return gui; + } + + public void setPreferencesStorage(PreferencesStorage preferencesStorage) { + this.preferencesStorage = preferencesStorage; + } + + public boolean contains(String filter) { + boolean contains = name.toLowerCase().contains(filter) + || ofNullable(tooltip).orElse("").toLowerCase().contains(filter) + || property != null && property.getValue().toString().toLowerCase().contains(filter); + return contains; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index 8270d55b..389ee915 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -29,11 +29,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane layout = SettingsTab.createGridLayout(); Settings settings = Config.getInstance().getSettings(); - Label l = new Label("Active"); - layout.add(l, 0, row); CheckBox enabled = new CheckBox(); enabled.setSelected(!settings.disabledSites.contains(myFreeCams.getName())); - enabled.setOnAction((e) -> { + enabled.setOnAction(e -> { if(enabled.isSelected()) { settings.disabledSites.remove(myFreeCams.getName()); } else { @@ -42,10 +40,13 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { save(); }); GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + Label l = new Label("Active"); + l.labelForProperty().set(enabled); + layout.add(l, 0, row); layout.add(enabled, 1, row++); - layout.add(new Label("MyFreeCams User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); + username.setId("mfcUsername"); username.setPrefWidth(300); username.textProperty().addListener((ob, o, n) -> { if(!n.equals(Config.getInstance().getSettings().mfcUsername)) { @@ -57,9 +58,11 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); + l = new Label("MyFreeCams User"); + l.labelForProperty().set(username); + layout.add(l, 0, row); layout.add(username, 1, row++); - layout.add(new Label("MyFreeCams Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().mfcPassword); password.textProperty().addListener((ob, o, n) -> { @@ -72,13 +75,15 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); + l = new Label("MyFreeCams Password"); + l.labelForProperty().set(password); + layout.add(l, 0, row); layout.add(password, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction(e -> DesktopIntegration.open(myFreeCams.getAffiliateLink())); layout.add(createAccount, 1, row++); - layout.add(new Label("MyFreeCams Base URL"), 0, row); TextField baseUrl = new TextField(); baseUrl.setText(Config.getInstance().getSettings().mfcBaseUrl); baseUrl.textProperty().addListener((ob, o, n) -> { @@ -88,6 +93,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(baseUrl, true); GridPane.setHgrow(baseUrl, Priority.ALWAYS); GridPane.setColumnSpan(baseUrl, 2); + l = new Label("MyFreeCams Base URL"); + l.labelForProperty().set(baseUrl); + layout.add(l, 0, row); layout.add(baseUrl, 1, row); GridPane.setColumnSpan(createAccount, 2); diff --git a/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java index 5ce7396d..48faf307 100644 --- a/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/tabs/RecordedModelsTab.java @@ -39,10 +39,11 @@ import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.StopRecordingAction; -import ctbrec.ui.controls.AutoFillTextField; import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.SearchBox; +import ctbrec.ui.controls.autocomplete.AutoFillTextField; +import ctbrec.ui.controls.autocomplete.ObservableListSuggester; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringPropertyBase; @@ -254,7 +255,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { modelLabel.setPadding(new Insets(5, 0, 0, 0)); ObservableList suggestions = FXCollections.observableArrayList(); sites.forEach(site -> suggestions.add(site.getClass().getSimpleName())); - model = new AutoFillTextField(suggestions); + model = new AutoFillTextField(new ObservableListSuggester(suggestions)); model.setPrefWidth(600); model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/"); model.onActionHandler(this::addModel);