From 59a238b8d0fc284e6099d9cfabd6034318b2e480 Mon Sep 17 00:00:00 2001 From: XP Date: Sun, 17 Dec 2023 14:49:00 -0800 Subject: [PATCH] New timeline sync editor --- .../timelines/CustomEventSyncController.java | 82 +++++++ .../timelines/CustomTimelineEntry.java | 16 +- .../timelines/EventSyncController.java | 16 +- .../timelines/FileEventSyncController.java | 10 + .../timelines/NullableEnumComboBox.java | 69 ++++++ .../xivsupport/timelines/TimelineManager.java | 7 +- .../xivsupport/timelines/TimelineUtils.java | 20 ++ .../timelines/cbevents/CbEventType.java | 4 + .../timelines/gui/SyncEditorGui.java | 216 ++++++++++++++++++ .../timelines/gui/TimelinesTab.java | 80 ++++++- .../gui/tables/StandardColumns.java | 14 +- 11 files changed, 521 insertions(+), 13 deletions(-) create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/CustomEventSyncController.java create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/NullableEnumComboBox.java create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineUtils.java create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/gui/SyncEditorGui.java diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomEventSyncController.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomEventSyncController.java new file mode 100644 index 000000000000..1fd5c2bc8e5f --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomEventSyncController.java @@ -0,0 +1,82 @@ +package gg.xp.xivsupport.timelines; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import gg.xp.reevent.events.Event; +import gg.xp.xivsupport.timelines.cbevents.CbEventFmt; +import gg.xp.xivsupport.timelines.cbevents.CbEventType; +import gg.xp.xivsupport.timelines.intl.LanguageReplacements; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CustomEventSyncController implements EventSyncController { + + private final Class eventClass; + private Map> conditions; + @JsonIgnore + private EventSyncController wrapped; + @JsonIgnore + private final CbEventType eventType; + + @JsonCreator + public CustomEventSyncController(@JsonProperty("type") CbEventType eventType, @JsonProperty("conditions") Map> conditions) { + this.eventType = eventType; + this.eventClass = eventType.eventType(); + this.conditions = new HashMap<>(conditions); + recalc(); + } + + public static CustomEventSyncController from(EventSyncController other) { + return new CustomEventSyncController(other.getType(), other.getRawConditions()); + } + + private void recalc() { + wrapped = CbEventFmt.parse(eventType, conditions); + } + + public void setConditions(Map> conditions) { + this.conditions = TimelineUtils.cloneConditions(conditions); + recalc(); + } + + @Override + public boolean shouldSync(Event event) { + return wrapped.shouldSync(event); + } + + @Override + public Class eventType() { + return eventClass; + } + + @Override + public EventSyncController translateWith(LanguageReplacements replacements) { + // No translation for custom entries + return this; + } + + @Override + public String toTextFormat() { + return wrapped.toTextFormat(); + } + + @Override + public CbEventType getType() { + return eventType; + } + + @Override + public Map> getRawConditions() { + return TimelineUtils.cloneConditions(conditions); + } + + @Override + public String toString() { + // TODO: there should be something to differentiate this from a builtin + return wrapped.toString(); + } +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java index c27e94833571..9b7795445739 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java @@ -27,6 +27,7 @@ public class CustomTimelineEntry implements CustomTimelineItem, Serializable { public double time; public @Nullable String name; public @Nullable Pattern sync; + public @Nullable CustomEventSyncController esc; public @Nullable Double duration; public @Nullable Double windowStart; public @Nullable Double windowEnd; @@ -52,6 +53,7 @@ public CustomTimelineEntry( double time, @Nullable String name, @Nullable Pattern sync, + @Nullable CustomEventSyncController esc, @Nullable Double duration, @NotNull TimelineWindow timelineWindow, @Nullable Double jump, @@ -68,6 +70,7 @@ public CustomTimelineEntry( this.time = time; this.name = name; this.sync = sync; + this.esc = esc; this.callout = callout; this.calloutPreTime = calloutPreTime; this.duration = duration; @@ -86,6 +89,7 @@ public CustomTimelineEntry( @JsonProperty("time") double time, @JsonProperty("name") @Nullable String name, @JsonProperty("sync") @Nullable String sync, + @JsonProperty("esc") @Nullable CustomEventSyncController esc, @JsonProperty("duration") @Nullable Double duration, @JsonProperty("timelineWindow") @NotNull TimelineWindow timelineWindow, @JsonProperty("jump") @Nullable Double jump, @@ -96,7 +100,7 @@ public CustomTimelineEntry( @JsonProperty(value = "disabled", defaultValue = "false") boolean disabled, @JsonProperty(value = "callout", defaultValue = "false") boolean callout, @JsonProperty(value = "calloutPreTime", defaultValue = "0") double calloutPreTime, - @JsonProperty(value = "jobs") @Nullable CombatJobSelection jobs + @JsonProperty("jobs") @Nullable CombatJobSelection jobs ) { // TODO: this wouldn't be a bad place to do the JAR url correction. Perhaps not the cleanest way, // but it works. @@ -115,6 +119,7 @@ public CustomTimelineEntry( this.replaces = replaces; this.enabled = !disabled; this.enabledJobs = jobs == null ? CombatJobSelection.all() : jobs; + this.esc = esc; } @Override @@ -196,9 +201,9 @@ public int hashCode() { } @Override + @JsonProperty("esc") public @Nullable EventSyncController eventSyncController() { - // TODO - return null; + return esc; } @Override @@ -207,6 +212,7 @@ public String toString() { "time=" + time + ", name='" + name + '\'' + ", sync=" + sync + + ", esc=" + esc + ", duration=" + duration + ", windowStart=" + windowStart + ", windowEnd=" + windowEnd + @@ -249,10 +255,12 @@ public static CustomTimelineEntry overrideFor(TimelineEntry other) { if (other.isLabel()) { throw new IllegalArgumentException("Cannot override a label with a real entry"); } + EventSyncController otherEsc = other.eventSyncController(); CustomTimelineEntry newCte = new CustomTimelineEntry( other.time(), other.name(), other.sync(), + otherEsc == null ? null : CustomEventSyncController.from(otherEsc), other.duration(), other.timelineWindow(), other.jump(), @@ -269,10 +277,12 @@ public static CustomTimelineEntry overrideFor(TimelineEntry other) { } public static CustomTimelineEntry cloneFor(TimelineEntry other) { + EventSyncController otherEsc = other.eventSyncController(); CustomTimelineEntry newCte = new CustomTimelineEntry( other.time(), other.name() + " copy", other.sync(), + otherEsc == null ? null : CustomEventSyncController.from(otherEsc), other.duration(), other.timelineWindow(), other.jump(), diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java index 033e1937eabe..9536f13d3ee6 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java @@ -1,18 +1,26 @@ package gg.xp.xivsupport.timelines; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import gg.xp.reevent.events.Event; +import gg.xp.xivsupport.timelines.cbevents.CbEventType; import gg.xp.xivsupport.timelines.intl.LanguageReplacements; +import java.util.List; +import java.util.Map; + public interface EventSyncController { boolean shouldSync(Event event); Class eventType(); - default boolean isEditable() { - return false; - }; - EventSyncController translateWith(LanguageReplacements replacements); String toTextFormat(); + + @JsonProperty("type") + CbEventType getType(); + + @JsonProperty("conditions") + Map> getRawConditions(); } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java index 170114599727..d1c098a71760 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java @@ -61,4 +61,14 @@ public String toTextFormat() { public String toString() { return type.displayName() + CbEventFmt.flattenListMap(original); } + + @Override + public CbEventType getType() { + return type; + } + + @Override + public Map> getRawConditions() { + return TimelineUtils.cloneConditions(original); + } } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/NullableEnumComboBox.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/NullableEnumComboBox.java new file mode 100644 index 000000000000..db6d2858501e --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/NullableEnumComboBox.java @@ -0,0 +1,69 @@ +package gg.xp.xivsupport.timelines; + +import gg.xp.xivsupport.gui.lists.FriendlyNameListCellRenderer; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.ListDataListener; +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class NullableEnumComboBox> extends JComboBox { + + private @Nullable X selectedItem; + + public NullableEnumComboBox(Class enumCls, String nullLabel, Consumer<@Nullable X> consumer, @Nullable X initialValue) { + X[] constants = enumCls.getEnumConstants(); + List list = new ArrayList<>(Arrays.asList(constants)); + list.add(0, null); + selectedItem = initialValue; + ComboBoxModel model = new ComboBoxModel<>() { + + @Override + public int getSize() { + return list.size(); + } + + @Override + public X getElementAt(int index) { + return list.get(index); + } + + @Override + public void addListDataListener(ListDataListener l) { + + } + + @Override + public void removeListDataListener(ListDataListener l) { + + } + + @Override + public void setSelectedItem(Object anItem) { + selectedItem = (X) anItem; + consumer.accept((X) anItem); + } + + @Override + public Object getSelectedItem() { + return selectedItem; + } + }; + setModel(model); + setRenderer(new FriendlyNameListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + if (value == null) { + return super.getListCellRendererComponent(list, nullLabel, index, isSelected, cellHasFocus); + } + return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + } + }); + + } + +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java index dd67e28f3fed..603315868ea0 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java @@ -194,7 +194,12 @@ private static String propStubForZoneId(long zoneId) { private final Map customizations = new ConcurrentHashMap<>(); public TimelineCustomizations getCustomSettings(long zoneId) { - return customizations.computeIfAbsent(zoneId, (k) -> pers.get(propStubForZoneId(k), TimelineCustomizations.class, new TimelineCustomizations())); + try { + return customizations.computeIfAbsent(zoneId, (k) -> pers.get(propStubForZoneId(k), TimelineCustomizations.class, new TimelineCustomizations())); + } + catch (Throwable t) { + throw new RuntimeException("Error loading timeline customizations for zone " + zoneId, t); + } } public void commitCustomSettings(long zoneId) { diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineUtils.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineUtils.java new file mode 100644 index 000000000000..f67a5e41703b --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineUtils.java @@ -0,0 +1,20 @@ +package gg.xp.xivsupport.timelines; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public final class TimelineUtils { + + private TimelineUtils() { + } + + public static Map> cloneConditions(Map> in) { + return in.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getKey, + e -> new ArrayList<>(e.getValue()) + )); + } + +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/cbevents/CbEventType.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/cbevents/CbEventType.java index f9afdad8bd71..b6ef8ef80f67 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/cbevents/CbEventType.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/cbevents/CbEventType.java @@ -200,4 +200,8 @@ public List> getFieldMappings() { return Collections.unmodifiableList(fieldMap); } } + + public List> getFieldMappings() { + return (List>) this.data.getFieldMappings(); + } } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/SyncEditorGui.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/SyncEditorGui.java new file mode 100644 index 000000000000..fbe3f505927f --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/SyncEditorGui.java @@ -0,0 +1,216 @@ +package gg.xp.xivsupport.timelines.gui; + +import gg.xp.xivsupport.gui.tables.CustomColumn; +import gg.xp.xivsupport.gui.tables.CustomTableModel; +import gg.xp.xivsupport.gui.tables.StandardColumns; +import gg.xp.xivsupport.gui.util.EasyAction; +import gg.xp.xivsupport.timelines.CustomEventSyncController; +import gg.xp.xivsupport.timelines.EventSyncController; +import gg.xp.xivsupport.timelines.NullableEnumComboBox; +import gg.xp.xivsupport.timelines.cbevents.CbEventType; +import gg.xp.xivsupport.timelines.cbevents.CbfMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class SyncEditorGui extends JDialog { + + private static final Logger log = LoggerFactory.getLogger(SyncEditorGui.class); + + private final CustomTableModel, List>> model; + private @Nullable CbEventType type; + private Map, List> conditions = Collections.emptyMap(); + private Result result; + + public SyncEditorGui(@NotNull Component parent, @Nullable EventSyncController initialValue) { + super(SwingUtilities.getWindowAncestor(parent), "Edit Sync"); + if (initialValue == null) { + setEventType(null); + } + else { + setEventType(initialValue.getType()); + Map> rawConds = initialValue.getRawConditions(); + conditions.replaceAll((k, v) -> { + List strings = rawConds.get(k.cbField()); + if (strings != null && !strings.isEmpty()) { + return new ArrayList<>(strings); + } + else { + return v; + } + }); + } + JPanel panel = new JPanel(new BorderLayout()); + JPanel top = new JPanel(); + JPanel buttons = new JPanel(); + + JButton submitButton = new EasyAction("OK", this::submit).asButton(); + JButton cancelButton = new EasyAction("Cancel", this::cancel).asButton(); + buttons.add(submitButton); + buttons.add(cancelButton); + + model = CustomTableModel.builder( + () -> { + List, List>> entries = new ArrayList<>(conditions.entrySet()); + entries.sort(Comparator.comparing(e -> e.getKey().ourLabel())); + return entries; + }) + .addColumn(new CustomColumn<>( + "Field", + f -> f.getKey().ourLabel() + )) + .addColumn(new CustomColumn<>( + "Cactbot Equivalent", + f -> f.getKey().cbField() + )) + .addColumn(new CustomColumn<>( + "Values (Separate with |)", + Map.Entry::getValue, + c -> { + c.setCellRenderer(new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (value == null) { + value = "(None/Any)"; + } + else if (value instanceof List listVal) { + if (listVal.isEmpty()) { + value = "(None/Any)"; + } + else { + value = String.join("|", (Iterable) listVal); + } + } + return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + } + }); + c.setCellEditor(StandardColumns., List>, List>customStringEditor( + (entry, value) -> entry.setValue(new ArrayList<>(value)), + val -> { + if (val == null || val.isEmpty()) { + return ""; + } + else { + return String.join("|", val); + } + }, + value -> Arrays.stream(value.split("\\|")).filter(s -> s != null && !s.isBlank()).toList() + )); + } + )) + .build(); + JTable table = new JTable(model) { + @Override + public boolean isCellEditable(int row, int column) { + return column == 2; + } + }; + model.configureColumns(table); + + JComboBox cb = new NullableEnumComboBox<>(CbEventType.class, "No Sync", this::setEventType, type); + top.add(cb); + + panel.add(new JScrollPane(table), BorderLayout.CENTER); + panel.add(top, BorderLayout.NORTH); + panel.add(buttons, BorderLayout.SOUTH); + panel.validate(); + panel.setPreferredSize(new Dimension(710, 400)); + setContentPane(panel); + pack(); + setModal(true); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setLocationRelativeTo(parent); + + } + + private void setEventType(@Nullable CbEventType type) { + this.type = type; + if (type == null) { + conditions = Collections.emptyMap(); + } + else { + conditions = type.getFieldMappings() + .stream() + .collect(Collectors.toMap( + Function.identity(), + cbf -> new ArrayList<>() + )); + } + refresh(); + } + + private void cancel() { + dispose(); + } + + private void submit() { + if (type == null) { + finish(new Result(true, null)); + } + else { + // Don't allow a condition-less event without user confirmation, as this is probably unintended + Map> finalValues = this.conditions.entrySet().stream() + .filter(e -> !e.getValue().isEmpty()) + .collect(Collectors.toMap( + e -> e.getKey().cbField(), + Map.Entry::getValue + )); + if (finalValues.isEmpty()) { + int confirmEmpty = JOptionPane.showConfirmDialog(this, "You didn't choose any conditions. Are you sure?", "No Conditions", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (confirmEmpty != JOptionPane.YES_OPTION) { + return; + } + } + finish(new Result(true, new CustomEventSyncController(type, finalValues))); + } + } + + private void finish(Result value) { + this.result = value; + setVisible(false); + dispose(); + } + + public static Result edit(@NotNull Component parent, @Nullable EventSyncController initialValue) { + SyncEditorGui dialog = new SyncEditorGui(parent, initialValue); + dialog.setVisible(true); + Result res = dialog.result; + if (res == null) { + log.error("null result!"); + return new Result(false, null); + } + return res; + } + + private void refresh() { + // refresh table + if (this.model != null) { + this.model.fullRefresh(); + } + } + + public static class Result { + public final boolean submitted; + public final @Nullable CustomEventSyncController value; + + public Result(boolean submitted, @Nullable CustomEventSyncController value) { + this.submitted = submitted; + this.value = value; + } + } + + +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/TimelinesTab.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/TimelinesTab.java index 80d3cc272178..51116c517f15 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/TimelinesTab.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/gui/TimelinesTab.java @@ -23,9 +23,11 @@ import gg.xp.xivsupport.persistence.gui.IntSettingSpinner; import gg.xp.xivsupport.persistence.gui.JobMultiSelectionGui; import gg.xp.xivsupport.sys.Threading; +import gg.xp.xivsupport.timelines.CustomEventSyncController; import gg.xp.xivsupport.timelines.CustomTimelineEntry; import gg.xp.xivsupport.timelines.CustomTimelineItem; import gg.xp.xivsupport.timelines.CustomTimelineLabel; +import gg.xp.xivsupport.timelines.EventSyncController; import gg.xp.xivsupport.timelines.TimelineCustomizations; import gg.xp.xivsupport.timelines.TimelineEntry; import gg.xp.xivsupport.timelines.TimelineInfo; @@ -160,14 +162,28 @@ public TimelinesTab(TimelineManager backend, TimelineOverlay overlay, XivState s })) .addColumn(new CustomColumn<>("Sync", e -> { if (e.sync() != null) { - return e.sync(); + return e.sync().pattern(); } if (e.eventSyncController() != null) { - return e.eventSyncController().toString(); + return e.eventSyncController(); } return null; }, col -> { - col.setCellEditor(noLabelNoNewSyncEdit(StandardColumns.regexEditorEmptyToNull(safeEditTimelineEntry(false, (item, value) -> item.sync = value), Pattern.CASE_INSENSITIVE))); + DefaultTableCellRenderer cellRenderer = new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + String displayValue; + if (isSelected) { + displayValue = value == null ? "Double Click to Add" : ("Double Click to Edit: " + value); + } + else { + displayValue = value == null ? "" : value.toString(); + } + return super.getTableCellRendererComponent(table, displayValue, isSelected, hasFocus, row, column); + } + }; + col.setCellRenderer(cellRenderer); + col.setCellEditor(new SyncCellEditor(cellRenderer)); })) .addColumn(new CustomColumn<>("Duration", TimelineEntry::duration, col -> { col.setCellEditor(noLabelEdit(StandardColumns.doubleEditorEmptyToNull(safeEditTimelineEntry(false, (item, value) -> item.duration = value)))); @@ -188,7 +204,7 @@ public TimelinesTab(TimelineManager backend, TimelineOverlay overlay, XivState s col.setPreferredWidth(numColPrefWidth); })) .addColumn(new CustomColumn<>("Win Effective", e -> { - if (e.timelineWindow() == TimelineWindow.NONE || e.sync() == null) { + if (e.timelineWindow() == TimelineWindow.NONE || !e.canSync()) { return ""; } return String.format("%.01f - %.01f", e.getMinTime(), e.getMaxTime()); @@ -978,6 +994,62 @@ public boolean isCellEditable(EventObject anEvent) { } + private class SyncCellEditor extends AbstractCellEditor implements TableCellEditor { + + @Serial + private static final long serialVersionUID = 188397433845199843L; + private final DefaultTableCellRenderer cellRenderer; + + private EventSyncController sel; + + public SyncCellEditor(DefaultTableCellRenderer cellRenderer) { + this.cellRenderer = cellRenderer; + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + TimelineEntry entry = ((CustomTableModel) table.getModel()).getValueForRow(row); + if (value instanceof EventSyncController esc) { + sel = CustomEventSyncController.from(esc); + } + else { + sel = null; + } + EventSyncController selTmp = sel; + Component out = cellRenderer.getTableCellRendererComponent(table, selTmp == null ? "" : selTmp.toString(), true, true, row, column); + SwingUtilities.invokeLater(() -> { + SyncEditorGui.Result result = SyncEditorGui.edit(TimelinesTab.this, selTmp); + if (result.submitted) { + safeEditTimelineEntry(false, (cte, unused) -> { + CustomEventSyncController val = result.value; + cte.esc = val; + if (val != null) { + cte.sync = null; + } + }).accept(entry, result.value); + } + SwingUtilities.invokeLater(TimelinesTab.this::stopEditing); + }); + return out; + } + + @Override + public Object getCellEditorValue() { + return sel; + } + + @Override + public boolean isCellEditable(EventObject anEvent) { + if (anEvent instanceof MouseEvent me) { + if (me.getClickCount() == 2) { + return true; + } + } + return false; + } + + } + private TableCellEditor noLabelNoNewSyncEdit(TableCellEditor wrapped) { return new RowConditionalTableCellEditor(wrapped, item -> !item.isLabel() && !item.hasEventSync()); } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java index 2ecea17f9693..2b520162caae 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java @@ -477,6 +477,10 @@ public static TableCellEditor urlEditorEmptyToNull(BiConsumer(writer, s -> s.isEmpty() ? null : makeUrl(s)); } + public static TableCellEditor customStringEditor(BiConsumer writer, Function formatter, Function parser) { + return new CustomEditor<>(writer, parser, formatter); + } + private static URL makeUrl(String url) { try { return new URL(url); @@ -501,16 +505,24 @@ private static class CustomEditor extends AbstractCellEditor implements Ta private static final long serialVersionUID = -3743763426515940614L; private final BiConsumer writer; private final Function parser; + private final Function formatter; public CustomEditor(BiConsumer writer, Function parser) { this.writer = writer; this.parser = parser; + formatter = String::valueOf; + } + + public CustomEditor(BiConsumer writer, Function parser, Function formatter) { + this.writer = writer; + this.parser = parser; + this.formatter = formatter; } @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { CustomTableModel model = (CustomTableModel) table.getModel(); - return new TextFieldWithValidation<>(parser, s -> writer.accept(model.getValueForRow(row), s), value == null ? "" : String.valueOf(value)); + return new TextFieldWithValidation<>(parser, s -> writer.accept(model.getValueForRow(row), s), value == null ? "" : formatter.apply((Y) value)); } @Override