285 lines
12 KiB
Erlang
285 lines
12 KiB
Erlang
%%% @doc
|
|
%%% A live modal for creating a SpendTX
|
|
%%% @end
|
|
|
|
-module(gd_m_spend).
|
|
-vsn("0.8.1").
|
|
-author("Craig Everett <zxq9@zxq9.com>").
|
|
-copyright("Craig Everett <zxq9@zxq9.com>").
|
|
-license("GPL-3.0-or-later").
|
|
|
|
%-behavior(zxw_modal).
|
|
|
|
-export([show/2]).
|
|
-export([init/1, handle_info/2, handle_event/2]).
|
|
|
|
-include_lib("wx/include/wx.hrl").
|
|
-include("$zx_include/zx_logger.hrl").
|
|
-include("gd.hrl").
|
|
|
|
-record(s,
|
|
{frame = none :: none | wx:wx_object(),
|
|
parent = none :: none | wx:wx_object(),
|
|
caller = none :: none | pid(),
|
|
j = j() :: fun(),
|
|
to_tx = none :: none | wx:wx_object(),
|
|
amount_tx = none :: none | wx:wx_object(),
|
|
payload_tx = none :: none | wx:wx_object(),
|
|
ttl_sl = none :: none | wx:wx_object(),
|
|
gas_sl = none :: none | wx:wx_object(),
|
|
affirm = none :: none | wx:wx_object(),
|
|
cancel = none :: none | wx:wx_object()}).
|
|
|
|
j() ->
|
|
fun(X) -> X end.
|
|
|
|
|
|
%%% Interface
|
|
|
|
-spec show(Parent, Args) -> {ok, #spend_tx{}} | cancel
|
|
when Parent :: wxFrame:wxFrame(),
|
|
Args :: {Account :: iolist(),
|
|
Nonce :: pos_integer(),
|
|
Height :: pos_integer(),
|
|
J :: fun()}.
|
|
|
|
show(Parent, Args) ->
|
|
zxw_modal:show(Parent, ?MODULE, Args).
|
|
|
|
|
|
|
|
%% Init
|
|
|
|
init({Parent, Caller, {Account, J}}) ->
|
|
Frame = wxFrame:new(Parent, ?wxID_ANY, J("Transfer Gajus")),
|
|
Panel = wxWindow:new(Frame, ?wxID_ANY),
|
|
TopSz = wxBoxSizer:new(?wxVERTICAL),
|
|
_ = wxBoxSizer:add(TopSz, Panel, zxw:flags(wide)),
|
|
MainSz = wxBoxSizer:new(?wxVERTICAL),
|
|
|
|
FromTx = wxStaticText:new(Panel, ?wxID_ANY, Account),
|
|
FromSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("From")}]),
|
|
_ = wxStaticBoxSizer:add(FromSz, FromTx, zxw:flags({wide, 5})),
|
|
ToTx = wxTextCtrl:new(Panel, ?wxID_ANY),
|
|
ToSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("To")}]),
|
|
_ = wxStaticBoxSizer:add(ToSz, ToTx, zxw:flags({wide, 5})),
|
|
AmtTx = wxTextCtrl:new(Panel, ?wxID_ANY),
|
|
AmtSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Amount")}]),
|
|
AmtInSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
_ = wxStaticBoxSizer:add(AmtInSz, AmtTx, zxw:flags({wide, 5})),
|
|
_ = wxStaticBoxSizer:add(AmtSz, AmtInSz, zxw:flags({wide, 5})),
|
|
PayloadTx = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]),
|
|
PayloadSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Message")}]),
|
|
_ = wxStaticBoxSizer:add(PayloadSz, PayloadTx, zxw:flags({wide, 5})),
|
|
Style = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}],
|
|
TTL_Sl = wxSlider:new(Panel, ?wxID_ANY, 100, 10, 1000, Style),
|
|
TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("TTL")}]),
|
|
_ = wxStaticBoxSizer:add(TTL_Sz, TTL_Sl, zxw:flags({wide, 5})),
|
|
Min = hz:min_gas_price(),
|
|
Max = Min * 2,
|
|
GasSl = wxSlider:new(Panel, ?wxID_ANY, Min, Min, Max, Style),
|
|
GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Gas Price")}]),
|
|
_ = wxStaticBoxSizer:add(GasSz, GasSl, zxw:flags({wide, 5})),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Panel, ?wxID_ANY, [{label, J("OK")}]),
|
|
Cancel = wxButton:new(Panel, ?wxID_ANY, [{label, J("Cancel")}]),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags({wide, 5})),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags({wide, 5})),
|
|
_ = wxBoxSizer:add(MainSz, FromSz, zxw:flags({base, 5})),
|
|
_ = wxBoxSizer:add(MainSz, ToSz, zxw:flags({base, 5})),
|
|
_ = wxBoxSizer:add(MainSz, AmtSz, zxw:flags({base, 5})),
|
|
_ = wxBoxSizer:add(MainSz, PayloadSz, zxw:flags({wide, 5})),
|
|
_ = wxBoxSizer:add(MainSz, TTL_Sz, zxw:flags({base, 5})),
|
|
_ = wxBoxSizer:add(MainSz, GasSz, zxw:flags({base, 5})),
|
|
_ = wxBoxSizer:add(MainSz, ButtSz, zxw:flags({base, 5})),
|
|
HandleKey = key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, Affirm, Cancel),
|
|
ok = wxFrame:connect(Frame, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(ToTx, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(AmtTx, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(PayloadTx, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(TTL_Sl, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(GasSl, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(Affirm, key_down, [{callback, HandleKey}]),
|
|
ok = wxTextCtrl:connect(Cancel, key_down, [{callback, HandleKey}]),
|
|
ok = wxFrame:connect(Frame, command_button_clicked),
|
|
ok = wxFrame:connect(Frame, close_window),
|
|
ok = wxFrame:setSize(Frame, {500, 650}),
|
|
ok = wxFrame:center(Frame),
|
|
ok = wxWindow:setSizer(Panel, MainSz),
|
|
ok = wxFrame:setSizer(Frame, TopSz),
|
|
ok = wxBoxSizer:layout(MainSz),
|
|
ok = wxFrame:centerOnParent(Frame),
|
|
true = wxFrame:show(Frame),
|
|
self() ! {focus, to_tx},
|
|
State =
|
|
#s{frame = Frame, parent = Parent, caller = Caller,
|
|
to_tx = ToTx, amount_tx = AmtTx, payload_tx = PayloadTx,
|
|
ttl_sl = TTL_Sl, gas_sl = GasSl,
|
|
affirm = Affirm, cancel = Cancel},
|
|
{Frame, State}.
|
|
|
|
|
|
key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, Affirm, Cancel) ->
|
|
Me = self(),
|
|
ToID = wxTextCtrl:getId(ToTx),
|
|
AmtID = wxTextCtrl:getId(AmtTx),
|
|
PL_ID = wxTextCtrl:getId(PayloadTx),
|
|
TTL_ID = wxSlider:getId(TTL_Sl),
|
|
GasID = wxSlider:getId(GasSl),
|
|
AffirmID = wxButton:getId(Affirm),
|
|
CancelID = wxButton:getId(Cancel),
|
|
fun(#wx{id = ID, event = #wxKey{type = key_down, keyCode = Code}}, KeyPress) ->
|
|
case {Code, wxKeyEvent:shiftDown(KeyPress)} of
|
|
{9, false} ->
|
|
case ID of
|
|
ToID -> Me ! {focus, amount_tx};
|
|
AmtID -> Me ! {focus, payload_tx};
|
|
PL_ID -> Me ! {focus, ttl_sl};
|
|
TTL_ID -> Me ! {focus, gas_sl};
|
|
GasID -> Me ! {focus, affirm};
|
|
AffirmID -> Me ! {focus, cancel};
|
|
CancelID -> Me ! {focus, to_tx};
|
|
_ -> wxEvent:skip(KeyPress)
|
|
end;
|
|
{9, true} ->
|
|
case ID of
|
|
ToID -> Me ! {focus, cancel};
|
|
AmtID -> Me ! {focus, to_tx};
|
|
PL_ID -> Me ! {focus, amount_tx};
|
|
TTL_ID -> Me ! {focus, payload_tx};
|
|
GasID -> Me ! {focus, ttl_sl};
|
|
AffirmID -> Me ! {focus, gas_sl};
|
|
CancelID -> Me ! {focus, affirm};
|
|
_ -> wxEvent:skip(KeyPress)
|
|
end;
|
|
{13, _} ->
|
|
case ID of
|
|
ToID -> Me ! {tab, to_tx};
|
|
AmtID -> Me ! {tab, amount_tx};
|
|
PL_ID -> Me ! {tab, payload_tx};
|
|
TTL_ID -> Me ! {tab, ttl_sl};
|
|
GasID -> Me ! {tab, gas_sl};
|
|
AffirmID -> Me ! {enter, affirm};
|
|
CancelID -> Me ! {enter, cancel};
|
|
_ -> wxEvent:skip(KeyPress)
|
|
end;
|
|
{27, _} ->
|
|
Me ! esc;
|
|
{_, _} ->
|
|
wxEvent:skip(KeyPress)
|
|
end
|
|
end.
|
|
|
|
|
|
|
|
handle_info({focus, Element}, State) ->
|
|
ok = focus(Element, State),
|
|
{noreply, State};
|
|
handle_info({enter, affirm}, State) ->
|
|
check(State);
|
|
handle_info({enter, cancel}, State) ->
|
|
cancel(State);
|
|
handle_info(esc, State) ->
|
|
ok = cancel(State),
|
|
{noreply, State};
|
|
handle_info(Message, State) ->
|
|
ok = log(warning, "Message: ~p~nState: ~p~n", [Message, State]),
|
|
{noreply, State}.
|
|
|
|
|
|
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
|
|
State = #s{affirm = Affirm, cancel = Cancel}) ->
|
|
AffirmID = wxButton:getId(Affirm),
|
|
CancelID = wxButton:getId(Cancel),
|
|
NewState =
|
|
case ID of
|
|
AffirmID -> check(State);
|
|
CancelID -> cancel(State)
|
|
end,
|
|
{noreply, NewState};
|
|
handle_event(#wx{event = #wxClose{}}, State) ->
|
|
NewState = cancel(State),
|
|
{noreply, NewState};
|
|
handle_event(Event, State) ->
|
|
ok = log(warning, "Unexpected event ~tp State: ~tp~n", [Event, State]),
|
|
{noreply, State}.
|
|
|
|
|
|
%%% Doers
|
|
|
|
focus(to_tx, #s{to_tx = ToTX}) -> wxTextCtrl:setFocus(ToTX);
|
|
focus(amount_tx, #s{amount_tx = AmountTX}) -> wxTextCtrl:setFocus(AmountTX);
|
|
focus(payload_tx, #s{payload_tx = PayloadTX}) -> wxTextCtrl:setFocus(PayloadTX);
|
|
focus(ttl_sl, #s{ttl_sl = TTL_SL}) -> wxSlider:setFocus(TTL_SL);
|
|
focus(gas_sl, #s{gas_sl = GasSL}) -> wxSlider:setFocus(GasSL);
|
|
focus(affirm, #s{affirm = Affirm}) -> wxButton:setFocus(Affirm);
|
|
focus(cancel, #s{cancel = Cancel}) -> wxButton:setFocus(Cancel).
|
|
|
|
|
|
cancel(#s{frame = Frame, caller = Caller}) ->
|
|
ok = wxFrame:destroy(Frame),
|
|
zxw_modal:done(Caller, cancel).
|
|
|
|
|
|
check(State =#s{frame = Frame, caller = Caller, j = J,
|
|
to_tx = ToTx, amount_tx = AmountTx, payload_tx = PayloadTx,
|
|
ttl_sl = TTL_Sl, gas_sl = GasSl}) ->
|
|
DirtyTX =
|
|
[{recipient_id, wxTextCtrl:getValue(ToTx)},
|
|
{amount, wxTextCtrl:getValue(AmountTx)},
|
|
{gas_price, wxSlider:getValue(GasSl)},
|
|
{ttl, wxSlider:getValue(TTL_Sl)},
|
|
{payload, wxTextCtrl:getValue(PayloadTx)}],
|
|
ok =
|
|
case clean_spend(DirtyTX, #spend_tx{}, J, []) of
|
|
{ok, CleanTX} ->
|
|
ok = wxFrame:destroy(Frame),
|
|
zxw_modal:done(Caller, {ok, CleanTX});
|
|
{error, Errors} ->
|
|
DerpyDerpDerp = form_message(Errors, J),
|
|
ok = zxw:show_message(Frame, DerpyDerpDerp),
|
|
State
|
|
end.
|
|
|
|
|
|
% TODO: There should be some suggestive logic around gas prices, both based on how large
|
|
% the payload and TTL are, but also the ingoing current common gas rate. This will require
|
|
% a "gas station" sort of analysis app to query, though.
|
|
clean_spend([{recipient_id, ""} | Rest], TX, J, Errors) ->
|
|
NewErrors = [{recipient_id, J("Recipient field is empty.")} | Errors],
|
|
clean_spend(Rest, TX, J, NewErrors);
|
|
clean_spend([{recipient_id, Recipient} | Rest], TX, J, Errors) ->
|
|
clean_spend(Rest, TX#spend_tx{recipient_id = list_to_binary(Recipient)}, J, Errors);
|
|
clean_spend([{amount, ""} | Rest], TX, J, Errors) ->
|
|
clean_spend(Rest, TX#spend_tx{amount = 0}, J, Errors);
|
|
clean_spend([{amount, S} | Rest], TX, J, Errors) ->
|
|
{NewTX, NewErrors} =
|
|
case hz_format:read(S) of
|
|
{ok, Amount} ->
|
|
{TX#spend_tx{amount = Amount}, Errors};
|
|
error ->
|
|
Derp = J("Amount field is not properly formatted."),
|
|
{TX, [{amount, Derp} | Errors]}
|
|
end,
|
|
clean_spend(Rest, NewTX, J, NewErrors);
|
|
clean_spend([{gas_price, GasPrice} | Rest], TX, J, Errors) ->
|
|
clean_spend(Rest, TX#spend_tx{gas_price = GasPrice}, J, Errors);
|
|
clean_spend([{ttl, TTL} | Rest], TX, J, Errors) ->
|
|
clean_spend(Rest, TX#spend_tx{ttl = TTL}, J, Errors);
|
|
clean_spend([{payload, S} | Rest], TX, J, Errors) ->
|
|
clean_spend(Rest, TX#spend_tx{payload = list_to_binary(S)}, J, Errors);
|
|
clean_spend([], TX, _, []) ->
|
|
{ok, TX};
|
|
clean_spend([], _, _, Errors) ->
|
|
{error, Errors}.
|
|
|
|
|
|
form_message(Errors, J) ->
|
|
Header = J("The following errors were encountered:"),
|
|
unicode:characters_to_list([Header, "\n", assemble(Errors)]).
|
|
|
|
% TODO: Highlight the fields since we know them.
|
|
assemble([{_, M} | T]) -> [M, "\n" | assemble(T)];
|
|
assemble([]) -> [].
|
|
|