From b6d172980d63ca3fa6880f152373de46deaaadcc Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Tue, 19 Nov 2019 13:44:26 +0100 Subject: [PATCH] tui: automatic config to TUI, step 1 --- .../nikiroo/fanfix/reader/tui/ConfigItem.java | 362 ++++++++++++++++++ .../fanfix/reader/tui/ConfigItemString.java | 50 +++ .../fanfix/reader/tui/TOptionWindow.java | 120 ++++++ .../reader/tui/TSimpleScrollableWindow.java | 119 ++++++ .../reader/tui/TuiReaderApplication.java | 18 + .../reader/tui/TuiReaderOptionWindow.java | 16 + 6 files changed, 685 insertions(+) create mode 100644 src/be/nikiroo/fanfix/reader/tui/ConfigItem.java create mode 100644 src/be/nikiroo/fanfix/reader/tui/ConfigItemString.java create mode 100644 src/be/nikiroo/fanfix/reader/tui/TOptionWindow.java create mode 100644 src/be/nikiroo/fanfix/reader/tui/TSimpleScrollableWindow.java create mode 100644 src/be/nikiroo/fanfix/reader/tui/TuiReaderOptionWindow.java diff --git a/src/be/nikiroo/fanfix/reader/tui/ConfigItem.java b/src/be/nikiroo/fanfix/reader/tui/ConfigItem.java new file mode 100644 index 0000000..43e9fe8 --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/tui/ConfigItem.java @@ -0,0 +1,362 @@ +package be.nikiroo.fanfix.reader.tui; + +import java.util.List; + +import jexer.TAction; +import jexer.TButton; +import jexer.TLabel; +import jexer.TPanel; +import jexer.TWidget; +import be.nikiroo.utils.resources.Bundle; +import be.nikiroo.utils.resources.MetaInfo; +import be.nikiroo.utils.ui.ConfigItemBase; + +/** + * A graphical item that reflect a configuration option from the given + * {@link Bundle}. + *

+ * This graphical item can be edited, and the result will be saved back into the + * linked {@link MetaInfo}; you still have to save the {@link MetaInfo} should + * you wish to, of course. + * + * @author niki + * + * @param + * the type of {@link Bundle} to edit + */ +public abstract class ConfigItem> extends TWidget { + /** The code base */ + private final ConfigItemBase base; + + /** + * Prepare a new {@link ConfigItem} instance, linked to the given + * {@link MetaInfo}. + * + * @param parent + * the parent widget + * @param info + * the info + * @param autoDirtyHandling + * TRUE to automatically manage the setDirty/Save operations, + * FALSE if you want to do it yourself via + * {@link ConfigItem#setDirtyItem(int)} + */ + protected ConfigItem(TWidget parent, MetaInfo info, + boolean autoDirtyHandling) { + super(parent); + + base = new ConfigItemBase(info, autoDirtyHandling) { + @Override + protected TWidget createEmptyField(int item) { + return ConfigItem.this.createEmptyField(item); + } + + @Override + protected Object getFromInfo(int item) { + return ConfigItem.this.getFromInfo(item); + } + + @Override + protected void setToInfo(Object value, int item) { + ConfigItem.this.setToInfo(value, item); + } + + @Override + protected Object getFromField(int item) { + return ConfigItem.this.getFromField(item); + } + + @Override + protected void setToField(Object value, int item) { + ConfigItem.this.setToField(value, item); + } + + @Override + public TWidget createField(int item) { + TWidget field = super.createField(item); + + // TODO: size? + + return field; + } + + @Override + public List reload() { + List removed = base.reload(); + if (!removed.isEmpty()) { + for (TWidget c : removed) { + removeChild(c); + } + } + + return removed; + } + }; + } + + /** + * Create a new {@link ConfigItem} for the given {@link MetaInfo}. + * + * @param nhgap + * negative horisontal gap in pixel to use for the label, i.e., + * the step lock sized labels will start smaller by that amount + * (the use case would be to align controls that start at a + * different horisontal position) + */ + public void init(int nhgap) { + if (getInfo().isArray()) { + // TODO: width + int size = getInfo().getListSize(false); + final TPanel pane = new TPanel(this, 0, 0, 20, size + 2); + final TWidget label = label(0, 0, nhgap); + label.setParent(pane, false); + setHeight(pane.getHeight()); + + for (int i = 0; i < size; i++) { + // TODO: minusPanel + TWidget field = base.addItem(i, null); + field.setParent(pane, false); + field.setX(label.getWidth() + 1); + field.setY(i); + } + + // x, y + final TButton add = new TButton(pane, "+", label.getWidth() + 1, + size + 1, null); + TAction action = new TAction() { + @Override + public void DO() { + TWidget field = base.addItem(base.getFieldsSize(), null); + field.setParent(pane, false); + field.setX(label.getWidth() + 1); + field.setY(add.getY()); + add.setY(add.getY() + 1); + } + }; + add.setAction(action); + } else { + final TWidget label = label(0, 0, nhgap); + + TWidget field = base.createField(-1); + field.setX(label.getWidth() + 1); + field.setWidth(10); // TODO + + // TODO + setWidth(30); + setHeight(1); + } + } + + /** The {@link MetaInfo} linked to the field. */ + public MetaInfo getInfo() { + return base.getInfo(); + } + + /** + * Retrieve the associated graphical component that was created with + * {@link ConfigItemBase#createEmptyField(int)}. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the graphical component + */ + protected TWidget getField(int item) { + return base.getField(item); + } + + /** + * Manually specify that the given item is "dirty" and thus should be saved + * when asked. + *

+ * Has no effect if the class is using automatic dirty handling (see + * {@link ConfigItemBase#ConfigItem(MetaInfo, boolean)}). + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + */ + protected void setDirtyItem(int item) { + base.setDirtyItem(item); + } + + /** + * Check if the value changed since the last load/save into the linked + * {@link MetaInfo}. + *

+ * Note that we consider NULL and an Empty {@link String} to be equals. + * + * @param value + * the value to test + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return TRUE if it has + */ + protected boolean hasValueChanged(Object value, int item) { + return base.hasValueChanged(value, item); + } + + /** + * Create an empty graphical component to be used later by + * {@link ConfigItem#createField(int)}. + *

+ * Note that {@link ConfigItem#reload(int)} will be called after it was + * created by {@link ConfigItem#createField(int)}. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the graphical component + */ + abstract protected TWidget createEmptyField(int item); + + /** + * Get the information from the {@link MetaInfo} in the subclass preferred + * format. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the information in the subclass preferred format + */ + abstract protected Object getFromInfo(int item); + + /** + * Set the value to the {@link MetaInfo}. + * + * @param value + * the value in the subclass preferred format + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + */ + abstract protected void setToInfo(Object value, int item); + + /** + * The value present in the given item's related field in the subclass + * preferred format. + * + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + * + * @return the value present in the given item's related field in the + * subclass preferred format + */ + abstract protected Object getFromField(int item); + + /** + * Set the value (in the subclass preferred format) into the field. + * + * @param value + * the value in the subclass preferred format + * @param item + * the item number to get for an array of values, or -1 to get + * the whole value (has no effect if {@link MetaInfo#isArray()} + * is FALSE) + */ + abstract protected void setToField(Object value, int item); + + /** + * Create a label which width is constrained in lock steps. + * + * @param x + * the X position of the label + * @param y + * the Y position of the label + * @param nhgap + * negative horisontal gap in pixel to use for the label, i.e., + * the step lock sized labels will start smaller by that amount + * (the use case would be to align controls that start at a + * different horisontal position) + * + * @return the label + */ + protected TWidget label(int x, int y, int nhgap) { + // TODO: see Swing version for lock-step sizes + // TODO: see Swing version for help info-buttons + + String lbl = getInfo().getName(); + return new TLabel(this, lbl, x, y); + } + + /** + * Create a new {@link ConfigItem} for the given {@link MetaInfo}. + * + * @param + * the type of {@link Bundle} to edit + * + * @param x + * the X position of the item + * @param y + * the Y position of the item + * @param parent + * the parent widget to use for this one + * @param info + * the {@link MetaInfo} + * @param nhgap + * negative horisontal gap in pixel to use for the label, i.e., + * the step lock sized labels will start smaller by that amount + * (the use case would be to align controls that start at a + * different horisontal position) + * + * @return the new {@link ConfigItem} + */ + static public > ConfigItem createItem(TWidget parent, + int x, int y, MetaInfo info, int nhgap) { + + ConfigItem configItem; + switch (info.getFormat()) { + // TODO + // case BOOLEAN: + // configItem = new ConfigItemBoolean(info); + // break; + // case COLOR: + // configItem = new ConfigItemColor(info); + // break; + // case FILE: + // configItem = new ConfigItemBrowse(info, false); + // break; + // case DIRECTORY: + // configItem = new ConfigItemBrowse(info, true); + // break; + // case COMBO_LIST: + // configItem = new ConfigItemCombobox(info, true); + // break; + // case FIXED_LIST: + // configItem = new ConfigItemCombobox(info, false); + // break; + // case INT: + // configItem = new ConfigItemInteger(info); + // break; + // case PASSWORD: + // configItem = new ConfigItemPassword(info); + // break; + // case LOCALE: + // configItem = new ConfigItemLocale(info); + // break; + // case STRING: + default: + configItem = new ConfigItemString(parent, info); + break; + } + + configItem.init(nhgap); + configItem.setX(x); + configItem.setY(y); + + return configItem; + } +} diff --git a/src/be/nikiroo/fanfix/reader/tui/ConfigItemString.java b/src/be/nikiroo/fanfix/reader/tui/ConfigItemString.java new file mode 100644 index 0000000..b1057e9 --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/tui/ConfigItemString.java @@ -0,0 +1,50 @@ +package be.nikiroo.fanfix.reader.tui; + +import jexer.TField; +import jexer.TWidget; +import be.nikiroo.utils.resources.MetaInfo; + +class ConfigItemString> extends ConfigItem { + /** + * Create a new {@link ConfigItemString} for the given {@link MetaInfo}. + * + * @param info + * the {@link MetaInfo} + */ + public ConfigItemString(TWidget parent, MetaInfo info) { + super(parent, info, true); + } + + @Override + protected Object getFromField(int item) { + TField field = (TField) getField(item); + if (field != null) { + return field.getText(); + } + + return null; + } + + @Override + protected Object getFromInfo(int item) { + return getInfo().getString(item, false); + } + + @Override + protected void setToField(Object value, int item) { + TField field = (TField) getField(item); + if (field != null) { + field.setText(value == null ? "" : value.toString()); + } + } + + @Override + protected void setToInfo(Object value, int item) { + getInfo().setString((String) value, item); + } + + @Override + protected TWidget createEmptyField(int item) { + return new TField(this, 0, 0, 1, false); + } +} diff --git a/src/be/nikiroo/fanfix/reader/tui/TOptionWindow.java b/src/be/nikiroo/fanfix/reader/tui/TOptionWindow.java new file mode 100644 index 0000000..4bb67de --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/tui/TOptionWindow.java @@ -0,0 +1,120 @@ +package be.nikiroo.fanfix.reader.tui; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jexer.TAction; +import jexer.TApplication; +import jexer.TPanel; +import jexer.TWidget; +import jexer.event.TCommandEvent; +import be.nikiroo.utils.StringUtils; +import be.nikiroo.utils.resources.Bundle; +import be.nikiroo.utils.resources.MetaInfo; + +public class TOptionWindow> extends TSimpleScrollableWindow { + private List> items; + + public TOptionWindow(TApplication parent, Class type, + final Bundle bundle, String title) { + super(parent, title, 0, 0, CENTERED | RESIZABLE); + + getMainPane().addLabel(title, 0, 0); + + items = new ArrayList>(); + List> groupedItems = MetaInfo.getItems(type, bundle); + int y = 2; + for (MetaInfo item : groupedItems) { + // will populate this.items + y += addItem(getMainPane(), 5, y, item, 0).getHeight(); + } + + y++; + + setRealHeight(y + 1); + + getMainPane().addButton("Reset", 25, y, new TAction() { + @Override + public void DO() { + for (MetaInfo item : items) { + item.reload(); + } + } + }); + + getMainPane().addButton("Default", 15, y, new TAction() { + @Override + public void DO() { + Object snap = bundle.takeSnapshot(); + bundle.reload(true); + for (MetaInfo item : items) { + item.reload(); + } + bundle.reload(false); + bundle.restoreSnapshot(snap); + } + }); + + getMainPane().addButton("Save", 1, y, new TAction() { + @Override + public void DO() { + for (MetaInfo item : items) { + item.save(true); + } + + try { + bundle.updateFile(); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + }); + } + + private TWidget addItem(TWidget parent, int x, int y, MetaInfo item, + int nhgap) { + if (item.isGroup()) { + // TODO: width + int w = 80 - x; + + String name = item.getName(); + String info = item.getDescription(); + info = StringUtils.justifyTexts(info, w - 3); // -3 for borders + + final TPanel pane = new TPanel(parent, x, y, w, 1); + pane.addLabel(name, 0, 0); + + int h = 0; + if (!info.isEmpty()) { + h += info.split("\n").length + 1; // +1 for scroll + pane.addText(info + "\n", 0, 1, w, h); + } + + // +1 for the title + h++; + + int paneY = h; // for the info desc + for (MetaInfo subitem : item) { + paneY += addItem(pane, 4, paneY, subitem, nhgap + 11) + .getHeight(); + } + + pane.setHeight(paneY); + return pane; + } + + items.add(item); + return ConfigItem.createItem(parent, x, y, item, nhgap); + } + + @Override + public void onCommand(TCommandEvent command) { + if (command.getCmd().equals(TuiReaderApplication.CMD_EXIT)) { + TuiReaderApplication.close(this); + } else { + // Handle our own event if needed here + super.onCommand(command); + } + } +} diff --git a/src/be/nikiroo/fanfix/reader/tui/TSimpleScrollableWindow.java b/src/be/nikiroo/fanfix/reader/tui/TSimpleScrollableWindow.java new file mode 100644 index 0000000..48a225e --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/tui/TSimpleScrollableWindow.java @@ -0,0 +1,119 @@ +package be.nikiroo.fanfix.reader.tui; + +import jexer.TApplication; +import jexer.THScroller; +import jexer.TPanel; +import jexer.TScrollableWindow; +import jexer.TVScroller; +import jexer.TWidget; + +public class TSimpleScrollableWindow extends TScrollableWindow { + protected TPanel mainPane; + private int prevHorizontal = -1; + private int prevVertical = -1; + + public TSimpleScrollableWindow(TApplication application, String title, + int width, int height) { + this(application, title, width, height, 0, 0, 0); + } + + public TSimpleScrollableWindow(TApplication application, String title, + int width, int height, int flags) { + this(application, title, width, height, flags, 0, 0); + } + + // 0 = none (so, no scrollbar) + public TSimpleScrollableWindow(TApplication application, String title, + int width, int height, int flags, int realWidth, int realHeight) { + super(application, title, width, height, flags); + + mainPane = new TPanel(this, 0, 0, width, 80) { + @Override + public void draw() { + for (TWidget children : mainPane.getChildren()) { + int y = children.getY() + children.getHeight(); + int x = children.getX() + children.getWidth(); + boolean visible = (y > getVerticalValue()) + && (x > getHorizontalValue()); + children.setVisible(visible); + } + super.draw(); + } + }; + + // // TODO: test + // for (int i = 0; i < 80; i++) { + // mainPane.addLabel("ligne " + i, i, i); + // } + + setRealWidth(realWidth); + setRealHeight(realHeight); + placeScrollbars(); + } + + /** + * The main pane on which you can add other widgets for this scrollable + * window. + * + * @return the main pane + */ + public TPanel getMainPane() { + return mainPane; + } + + public void setRealWidth(int realWidth) { + if (realWidth <= 0) { + if (hScroller != null) { + hScroller.remove(); + } + } else { + if (hScroller == null) { + // size/position will be fixed by placeScrollbars() + hScroller = new THScroller(this, 0, 0, 10); + } + hScroller.setRightValue(realWidth); + } + + reflowData(); + } + + public void setRealHeight(int realHeight) { + if (realHeight <= 0) { + if (vScroller != null) { + vScroller.remove(); + } + } else { + if (vScroller == null) { + // size/position will be fixed by placeScrollbars() + vScroller = new TVScroller(this, 0, 0, 10); + } + vScroller.setBottomValue(realHeight); + } + + reflowData(); + } + + @Override + public void reflowData() { + super.reflowData(); + reflowData(getHorizontalValue(), getVerticalValue()); + } + + protected void reflowData(int totalX, int totalY) { + super.reflowData(); + mainPane.setX(-totalX); + mainPane.setY(-totalY); + } + + @Override + public void draw() { + if (prevHorizontal != getHorizontalValue() + || prevVertical != getVerticalValue()) { + prevHorizontal = getHorizontalValue(); + prevVertical = getVerticalValue(); + reflowData(); + } + + super.draw(); + } +} diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java index d8c9397..1e7e8eb 100644 --- a/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderApplication.java @@ -44,6 +44,10 @@ class TuiReaderApplication extends TApplication implements Reader { public static final int MENU_FILE_DELETE = 1029; public static final int MENU_FILE_LIBRARY = 1030; public static final int MENU_FILE_EXIT = 1031; + // + public static final int MENU_OPT_FANFIX = 1032; + public static final int MENU_OPT_TUI = 1033; + public static final TCommand CMD_EXIT = new TCommand(MENU_FILE_EXIT) { }; @@ -230,9 +234,14 @@ class TuiReaderApplication extends TApplication implements Reader { fileMenu.addItem(MENU_FILE_LIBRARY, "Lib&rary"); fileMenu.addSeparator(); fileMenu.addItem(MENU_FILE_EXIT, "E&xit"); + + TMenu OptionsMenu = addMenu("&Options"); + OptionsMenu.addItem(MENU_OPT_FANFIX, "&Fanfix Configuration"); + OptionsMenu.addItem(MENU_OPT_TUI, "&UI Configuration"); setStatusBar(fileMenu, "File-management " + "commands (Open, Save, Print, etc.)"); + // TODO: Edit: re-download, delete @@ -348,6 +357,15 @@ class TuiReaderApplication extends TApplication implements Reader { case MENU_FILE_LIBRARY: showMain(); return true; + + case MENU_OPT_FANFIX: + new TuiReaderOptionWindow(this, false).maximize(); + return true; + + case MENU_OPT_TUI: + new TuiReaderOptionWindow(this, true).maximize(); + return true; + } return super.onMenu(menu); diff --git a/src/be/nikiroo/fanfix/reader/tui/TuiReaderOptionWindow.java b/src/be/nikiroo/fanfix/reader/tui/TuiReaderOptionWindow.java new file mode 100644 index 0000000..a27cdbe --- /dev/null +++ b/src/be/nikiroo/fanfix/reader/tui/TuiReaderOptionWindow.java @@ -0,0 +1,16 @@ +package be.nikiroo.fanfix.reader.tui; + +import jexer.TStatusBar; +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.bundles.UiConfig; + +class TuiReaderOptionWindow extends TOptionWindow { + public TuiReaderOptionWindow(TuiReaderApplication reader, boolean uiOptions) { + super(reader, uiOptions ? UiConfig.class : Config.class, + uiOptions ? Instance.getUiConfig() : Instance.getConfig(), + "Options"); + + TStatusBar statusBar = reader.setStatusBar(this, "Options"); + } +} -- 2.27.0