store treetable column widths on adjustment, and restore on wallet load

This commit is contained in:
Craig Raw 2025-01-28 10:33:46 +02:00
parent 6a001bd67f
commit f9199b65f0
16 changed files with 200 additions and 17 deletions

2
drongo

@ -1 +1 @@
Subproject commit 378ab611f5bc7fc06e73d8c86003b4b7ea79751d Subproject commit 1805aeb3740a9b90cff321219f0e60c4a1ccd3a6

View file

@ -1,5 +1,7 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -77,7 +79,7 @@ public class AddressTreeTable extends CoinTreeTable {
getColumns().forEach(col -> col.setContextMenu(contextMenu)); getColumns().forEach(col -> col.setContextMenu(contextMenu));
setEditable(true); setEditable(true);
setEqualPreferredColumnWidths(); setupColumnWidths(rootEntry.getNode().getIndex() == KeyPurpose.RECEIVE.getPathIndex().num() ? TableType.RECEIVE_ADDRESSES : TableType.CHANGE_ADDRESSES);
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING); addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
getSortOrder().add(addressCol); getSortOrder().add(addressCol);

View file

@ -1,11 +1,14 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
import com.sparrowwallet.sparrow.CurrencyRate; import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletTableColumnsResizedEvent;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent; import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent; import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent; import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
@ -13,19 +16,19 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType; import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.Entry;
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Hyperlink; import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
public class CoinTreeTable extends TreeTableView<Entry> { public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit; private BitcoinUnit bitcoinUnit;
@ -33,6 +36,9 @@ public class CoinTreeTable extends TreeTableView<Entry> {
private CurrencyRate currencyRate; private CurrencyRate currencyRate;
protected static final double STANDARD_WIDTH = 100.0; protected static final double STANDARD_WIDTH = 100.0;
private final PublishSubject<WalletTableColumnsResizedEvent> columnResizeSubject = PublishSubject.create();
private final Observable<WalletTableColumnsResizedEvent> columnResizeEvents = columnResizeSubject.debounce(1, TimeUnit.SECONDS);
public BitcoinUnit getBitcoinUnit() { public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit; return bitcoinUnit;
} }
@ -146,10 +152,36 @@ public class CoinTreeTable extends TreeTableView<Entry> {
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
protected void setEqualPreferredColumnWidths() { protected void setupColumnWidths(TableType tableType) {
for(TreeTableColumn<?, ?> column : getColumns()) { Double[] savedWidths = getSavedColumnWidths(tableType);
column.setPrefWidth(STANDARD_WIDTH); for(int i = 0; i < getColumns().size(); i++) {
TreeTableColumn<Entry, ?> column = getColumns().get(i);
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
} }
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
getColumns().getLast().widthProperty().addListener((_, _, _) -> {
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
Double[] widths = getColumns().stream().map(TableColumnBase::getWidth).toArray(Double[]::new);
WalletTable walletTable = new WalletTable(tableType, widths);
columnResizeSubject.onNext(new WalletTableColumnsResizedEvent(getRoot().getValue().getWallet(), walletTable));
}
});
columnResizeEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> EventManager.get().post(event));
}
private Double[] getSavedColumnWidths(TableType tableType) {
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
Wallet wallet = getRoot().getValue().getWallet();
WalletTable walletTable = wallet.getWalletTable(tableType);
if(walletTable != null) {
return walletTable.getWidths();
}
}
return null;
} }
} }

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
@ -89,7 +90,6 @@ public class SearchWalletDialog extends Dialog<Entry> {
results.setShowRoot(false); results.setShowRoot(false);
results.setPrefWidth(showWallet || showAccount ? 950 : 850); results.setPrefWidth(showWallet || showAccount ? 950 : 850);
results.setUnitFormat(walletForms.iterator().next().getWallet()); results.setUnitFormat(walletForms.iterator().next().getWallet());
results.setEqualPreferredColumnWidths();
results.setPlaceholder(new Label("No results")); results.setPlaceholder(new Label("No results"));
results.setEditable(true); results.setEditable(true);
@ -169,7 +169,12 @@ public class SearchWalletDialog extends Dialog<Entry> {
searchWallets(newValue); searchWallets(newValue);
}); });
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.getFirst().getWallet(), Collections.emptyList());
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
results.setRoot(rootItem);
setResizable(true); setResizable(true);
results.setupColumnWidths(TableType.SEARCH_WALLET);
AppServices.moveToActiveWindowScreen(this); AppServices.moveToActiveWindowScreen(this);

View file

@ -1,11 +1,11 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry; import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry; import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
public class TransactionsTreeTable extends CoinTreeTable { public class TransactionsTreeTable extends CoinTreeTable {
public void initialize(WalletTransactionsEntry rootEntry) { public void initialize(WalletTransactionsEntry rootEntry) {
@ -49,7 +49,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet())); setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true); setEditable(true);
setEqualPreferredColumnWidths(); setupColumnWidths(TableType.TRANSACTIONS);
setSortColumn(0, TreeTableColumn.SortType.DESCENDING); setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
} }

View file

@ -1,10 +1,10 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.sparrow.wallet.*; import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.scene.control.SelectionMode; import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import java.util.Comparator; import java.util.Comparator;
@ -82,7 +82,7 @@ public class UtxosTreeTable extends CoinTreeTable {
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet())); setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true); setEditable(true);
setEqualPreferredColumnWidths(); setupColumnWidths(TableType.UTXOS);
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING); setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.CurrencyRate; import com.sparrowwallet.sparrow.CurrencyRate;
@ -101,7 +102,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
table.setRoot(rootItem); table.setRoot(rootItem);
rootItem.setExpanded(true); rootItem.setExpanded(true);
table.setEqualPreferredColumnWidths(); table.setupColumnWidths(TableType.WALLET_SUMMARY);
table.setPrefWidth(450); table.setPrefWidth(450);
VBox vBox = new VBox(); VBox vBox = new VBox();

View file

@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
public class WalletTableColumnsResizedEvent extends WalletChangedEvent {
private final WalletTable walletTable;
public WalletTableColumnsResizedEvent(Wallet wallet, WalletTable walletTable) {
super(wallet);
this.walletTable = walletTable;
}
public WalletTable getWalletTable() {
return walletTable;
}
public TableType getTableType() {
return walletTable == null ? null : walletTable.getTableType();
}
}

View file

@ -323,6 +323,11 @@ public class DbPersistence implements Persistence {
walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig()); walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig());
} }
if(dirtyPersistables.walletTable != null) {
WalletTableDao walletTableDao = handle.attach(WalletTableDao.class);
walletTableDao.addOrUpdate(wallet, dirtyPersistables.walletTable.getTableType(), dirtyPersistables.walletTable);
}
if(dirtyPersistables.mixConfig) { if(dirtyPersistables.mixConfig) {
MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class); MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class);
mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig()); mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig());
@ -768,6 +773,13 @@ public class DbPersistence implements Persistence {
} }
} }
@Subscribe
public void walletTableColumnsResized(WalletTableColumnsResizedEvent event) {
if(persistsFor(event.getWallet()) && event.getTableType() != null && event.getWallet().getWalletTable(event.getTableType()) != null) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).walletTable = event.getWalletTable());
}
}
@Subscribe @Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) { public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) { if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
@ -824,6 +836,7 @@ public class DbPersistence implements Persistence {
public final List<Entry> labelEntries = new ArrayList<>(); public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>(); public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public boolean walletConfig; public boolean walletConfig;
public WalletTable walletTable = null;
public boolean mixConfig; public boolean mixConfig;
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>(); public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>(); public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
@ -845,6 +858,7 @@ public class DbPersistence implements Persistence {
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) + "\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
"\nUTXO statuses:" + utxoStatuses + "\nUTXO statuses:" + utxoStatuses +
"\nWallet config:" + walletConfig + "\nWallet config:" + walletConfig +
"\nWallet table:" + walletTable +
"\nMix config:" + mixConfig + "\nMix config:" + mixConfig +
"\nUTXO mixes changed:" + changedUtxoMixes + "\nUTXO mixes changed:" + changedUtxoMixes +
"\nUTXO mixes removed:" + removedUtxoMixes + "\nUTXO mixes removed:" + removedUtxoMixes +

View file

@ -33,6 +33,9 @@ public interface WalletDao {
@CreateSqlObject @CreateSqlObject
WalletConfigDao createWalletConfigDao(); WalletConfigDao createWalletConfigDao();
@CreateSqlObject
WalletTableDao createWalletTableDao();
@CreateSqlObject @CreateSqlObject
MixConfigDao createMixConfigDao(); MixConfigDao createMixConfigDao();
@ -119,6 +122,10 @@ public interface WalletDao {
wallet.getDetachedLabels().putAll(detachedLabels); wallet.getDetachedLabels().putAll(detachedLabels);
wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId())); wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId()));
Map<TableType, WalletTable> walletTables = createWalletTableDao().getForWalletId(wallet.getId());
wallet.getWalletTables().putAll(walletTables);
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId())); wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId()); Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
@ -138,6 +145,7 @@ public interface WalletDao {
createBlockTransactionDao().addBlockTransactions(wallet); createBlockTransactionDao().addBlockTransactions(wallet);
createDetachedLabelDao().clearAndAddAll(wallet); createDetachedLabelDao().clearAndAddAll(wallet);
createWalletConfigDao().addWalletConfig(wallet); createWalletConfigDao().addWalletConfig(wallet);
createWalletTableDao().addWalletTables(wallet);
createMixConfigDao().addMixConfig(wallet); createMixConfigDao().addMixConfig(wallet);
createUtxoMixDataDao().addUtxoMixData(wallet); createUtxoMixDataDao().addUtxoMixData(wallet);
} finally { } finally {

View file

@ -0,0 +1,50 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletTable;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.HashMap;
import java.util.Map;
public interface WalletTableDao {
@SqlQuery("select id, type, widths from walletTable where wallet = ?")
@RegisterRowMapper(WalletTableMapper.class)
Map<TableType, WalletTable> getForWalletId(Long id);
@SqlQuery("select id, type, widths from walletTable where type = ?")
@RegisterRowMapper(WalletTableMapper.class)
Map<TableType, WalletTable> getForTypeId(int tableTypeId);
@SqlUpdate("insert into walletTable (type, widths, wallet) values (?, ?, ?)")
@GetGeneratedKeys("id")
long insertWalletTable(int tableType, Double[] widths, long wallet);
@SqlUpdate("update walletTable set type = ?, widths = ?, wallet = ? where id = ?")
void updateWalletTable(int tableType, Double[] widths, long wallet, long id);
default void addWalletTables(Wallet wallet) {
Map<TableType, WalletTable> walletTables = new HashMap<>(wallet.getWalletTables());
for(Map.Entry<TableType, WalletTable> tableEntry : walletTables.entrySet()) {
tableEntry.getValue().setId(null);
addOrUpdate(wallet, tableEntry.getKey(), tableEntry.getValue());
}
}
default void addOrUpdate(Wallet wallet, TableType tableType, WalletTable walletTable) {
Map<TableType, WalletTable> existing = getForTypeId(tableType.ordinal());
if(existing.isEmpty() && walletTable.getId() == null) {
long id = insertWalletTable(walletTable.getTableType().ordinal(), walletTable.getWidths(), wallet.getId());
walletTable.setId(id);
} else {
Long existingId = existing.get(tableType) != null ? existing.get(tableType).getId() : walletTable.getId();
updateWalletTable(walletTable.getTableType().ordinal(), walletTable.getWidths(), wallet.getId(), existingId);
walletTable.setId(existingId);
}
}
}

View file

@ -0,0 +1,40 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.WalletTable;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
public class WalletTableMapper implements RowMapper<Map.Entry<TableType, WalletTable>> {
@Override
public Map.Entry<TableType, WalletTable> map(ResultSet rs, StatementContext ctx) throws SQLException {
TableType tableType = TableType.values()[rs.getInt("type")];
Object[] objWidths = (Object[])rs.getArray("widths").getArray();
Double[] widths = Arrays.copyOf(objWidths, objWidths.length, Double[].class);
WalletTable walletTable = new WalletTable(tableType, widths);
walletTable.setId(rs.getLong("id"));
return new Map.Entry<>() {
@Override
public TableType getKey() {
return tableType;
}
@Override
public WalletTable getValue() {
return walletTable;
}
@Override
public WalletTable setValue(WalletTable value) {
return null;
}
};
}
}

View file

@ -593,6 +593,14 @@ public class WalletForm {
} }
} }
@Subscribe
public void walletTableColumnsResized(WalletTableColumnsResizedEvent event) {
if(event.getWallet() == wallet && event.getTableType() != null) {
wallet.getWalletTables().put(event.getTableType(), event.getWalletTable());
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe @Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) { public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(event.getWallet() == wallet) { if(event.getWallet() == wallet) {

View file

@ -1 +0,0 @@
alter table keystore add column deviceRegistration varbinary(32) after externalPaymentCode;

View file

@ -0,0 +1,2 @@
create table walletTable (id identity not null, type integer not null, widths double precision array not null, wallet bigint not null);
alter table keystore add column deviceRegistration varbinary(32) after externalPaymentCode;