%%% @doc %%% A live modal for creating a SpendTX %%% @end -module(gd_m_spend). -vsn("0.8.1"). -author("Craig Everett "). -copyright("Craig Everett "). -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([]) -> [].