diff --git a/applications/plugins/protoview/app.c b/applications/plugins/protoview/app.c index aa2cdf8a6..d36e21291 100644 --- a/applications/plugins/protoview/app.c +++ b/applications/plugins/protoview/app.c @@ -64,10 +64,13 @@ static void render_callback(Canvas* const canvas, void* ctx) { case ViewDirectSampling: render_view_direct_sampling(canvas, app); break; - case ViewLast: - furi_crash(TAG " ViewLast selected"); + default: + furi_crash(TAG "Invalid view selected"); break; } + + /* Draw the alert box if set. */ + ui_draw_alert_if_needed(canvas, app); } /* Here all we do is putting the events into the queue that will be handled @@ -79,17 +82,23 @@ static void input_callback(InputEvent* input_event, void* ctx) { /* Called to switch view (when left/right is pressed). Handles * changing the current view ID and calling the enter/exit view - * callbacks if needed. */ -static void app_switch_view(ProtoViewApp* app, SwitchViewDirection dir) { + * callbacks if needed. + * + * The 'switchto' parameter can be the identifier of a view, or the + * special views ViewGoNext and ViewGoPrev in order to move to + * the logical next/prev view. */ +static void app_switch_view(ProtoViewApp* app, ProtoViewCurrentView switchto) { ProtoViewCurrentView old = app->current_view; - if(dir == AppNextView) { + if(switchto == ViewGoNext) { app->current_view++; if(app->current_view == ViewLast) app->current_view = 0; - } else if(dir == AppPrevView) { + } else if(switchto == ViewGoPrev) { if(app->current_view == 0) app->current_view = ViewLast - 1; else app->current_view--; + } else { + app->current_view = switchto; } ProtoViewCurrentView new = app->current_view; @@ -108,6 +117,7 @@ static void app_switch_view(ProtoViewApp* app, SwitchViewDirection dir) { * the main thing. */ app->current_subview[old] = 0; memset(app->view_privdata, 0, PROTOVIEW_VIEW_PRIVDATA_LEN); + ui_dismiss_alert(app); } /* Allocate the application state and initialize a number of stuff. @@ -134,6 +144,7 @@ ProtoViewApp* protoview_app_alloc() { app->view_dispatcher = NULL; app->text_input = NULL; app->show_text_input = false; + app->alert_dismiss_time = 0; app->current_view = ViewRawPulses; for(int j = 0; j < ViewLast; j++) app->current_subview[j] = 0; app->direct_sampling_enabled = false; @@ -266,18 +277,26 @@ int32_t protoview_app_entry(void* p) { /* Handle navigation here. Then handle view-specific inputs * in the view specific handling function. */ if(input.type == InputTypeShort && input.key == InputKeyBack) { - /* Exit the app. */ + if(app->current_view != ViewRawPulses) { + /* If this is not the main app view, go there. */ + app_switch_view(app, ViewRawPulses); + } else { + /* If we are in the main app view, warn the user + * they needs to long press to really quit. */ + ui_show_alert(app, "Long press to exit", 1000); + } + } else if(input.type == InputTypeLong && input.key == InputKeyBack) { app->running = 0; } else if( input.type == InputTypeShort && input.key == InputKeyRight && - get_current_subview(app) == 0) { + ui_get_current_subview(app) == 0) { /* Go to the next view. */ - app_switch_view(app, AppNextView); + app_switch_view(app, ViewGoNext); } else if( input.type == InputTypeShort && input.key == InputKeyLeft && - get_current_subview(app) == 0) { + ui_get_current_subview(app) == 0) { /* Go to the previous view. */ - app_switch_view(app, AppPrevView); + app_switch_view(app, ViewGoPrev); } else { /* This is where we pass the control to the currently * active view input processing. */ @@ -295,8 +314,8 @@ int32_t protoview_app_entry(void* p) { case ViewDirectSampling: process_input_direct_sampling(app, input); break; - case ViewLast: - furi_crash(TAG " ViewLast selected"); + default: + furi_crash(TAG "Invalid view selected"); break; } } diff --git a/applications/plugins/protoview/app.h b/applications/plugins/protoview/app.h index 60fc12546..9242dec33 100644 --- a/applications/plugins/protoview/app.h +++ b/applications/plugins/protoview/app.h @@ -48,10 +48,12 @@ typedef enum { ViewModulationSettings, ViewDirectSampling, ViewLast, /* Just a sentinel to wrap around. */ -} ProtoViewCurrentView; -/* Used by app_switch_view() */ -typedef enum { AppNextView, AppPrevView } SwitchViewDirection; + /* The following are special views that are not iterated, but + * have meaning for the API. */ + ViewGoNext, + ViewGoPrev, +} ProtoViewCurrentView; typedef struct { const char* name; // Name to show to the user. @@ -114,6 +116,7 @@ typedef struct ProtoViewMsgInfo { } ProtoViewMsgInfo; /* Our main application context. */ +#define ALERT_MAX_LEN 32 struct ProtoViewApp { /* GUI */ Gui* gui; @@ -123,6 +126,8 @@ struct ProtoViewApp { ProtoViewCurrentView current_view; /* Active left-right view ID. */ int current_subview[ViewLast]; /* Active up-down subview ID. */ FuriMessageQueue* event_queue; /* Keypress events go here. */ + + /* Input text state. */ ViewDispatcher* view_dispatcher; /* Used only when we want to show the text_input view for a moment. Otherwise it is set to null. */ @@ -132,6 +137,12 @@ struct ProtoViewApp { uint32_t text_input_buffer_len; void (*text_input_done_callback)(void*); + /* Alert state. */ + uint32_t alert_dismiss_time; /* Millisecond when the alert will be + no longer shown. Or zero if the alert + is currently not set at all. */ + char alert_text[ALERT_MAX_LEN]; /* Alert content. */ + /* Radio related. */ ProtoViewTxRx* txrx; /* Radio state. */ SubGhzSetting* setting; /* A list of valid frequencies. */ @@ -245,9 +256,18 @@ void view_exit_direct_sampling(ProtoViewApp* app); void view_exit_settings(ProtoViewApp* app); /* ui.c */ -int get_current_subview(ProtoViewApp* app); -void show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview); -bool process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subview); +int ui_get_current_subview(ProtoViewApp* app); +void ui_show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview); +bool ui_process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subview); +void ui_show_keyboard( + ProtoViewApp* app, + char* buffer, + uint32_t buflen, + void (*done_callback)(void*)); +void ui_dismiss_keyboard(ProtoViewApp* app); +void ui_show_alert(ProtoViewApp* app, const char* text, uint32_t ttl); +void ui_dismiss_alert(ProtoViewApp* app); +void ui_draw_alert_if_needed(Canvas* canvas, ProtoViewApp* app); void canvas_draw_str_with_border( Canvas* canvas, uint8_t x, @@ -255,8 +275,6 @@ void canvas_draw_str_with_border( const char* str, Color text_color, Color border_color); -void show_keyboard(ProtoViewApp* app, char* buffer, uint32_t buflen, void (*done_callback)(void*)); -void dismiss_keyboard(ProtoViewApp* app); /* crc.c */ uint8_t crc8(const uint8_t* data, size_t len, uint8_t init, uint8_t poly); diff --git a/applications/plugins/protoview/ui.c b/applications/plugins/protoview/ui.c index e2c775d96..b0251f09f 100644 --- a/applications/plugins/protoview/ui.c +++ b/applications/plugins/protoview/ui.c @@ -10,15 +10,15 @@ /* Return the ID of the currently selected subview, of the current * view. */ -int get_current_subview(ProtoViewApp* app) { +int ui_get_current_subview(ProtoViewApp* app) { return app->current_subview[app->current_view]; } /* Called by view rendering callback that has subviews, to show small triangles * facing down/up if there are other subviews the user can access with up * and down. */ -void show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview) { - int subview = get_current_subview(app); +void ui_show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview) { + int subview = ui_get_current_subview(app); if(subview != 0) canvas_draw_triangle(canvas, 120, 5, 8, 5, CanvasDirectionBottomToTop); if(subview != last_subview - 1) canvas_draw_triangle(canvas, 120, 59, 8, 5, CanvasDirectionTopToBottom); @@ -27,8 +27,8 @@ void show_available_subviews(Canvas* canvas, ProtoViewApp* app, int last_subview /* Handle up/down keys when we are in a subview. If the function catched * such keypress, it returns true, so that the actual view input callback * knows it can just return ASAP without doing anything. */ -bool process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subview) { - int subview = get_current_subview(app); +bool ui_process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subview) { + int subview = ui_get_current_subview(app); if(input.type == InputTypePress) { if(input.key == InputKeyUp) { if(subview != 0) app->current_subview[app->current_view]--; @@ -57,17 +57,69 @@ bool process_subview_updown(ProtoViewApp* app, InputEvent input, int last_subvie * * Note: if the buffer is not a null-termined zero string, what it contains will * be used as initial input for the user. */ -void show_keyboard(ProtoViewApp* app, char* buffer, uint32_t buflen, void (*done_callback)(void*)) { +void ui_show_keyboard( + ProtoViewApp* app, + char* buffer, + uint32_t buflen, + void (*done_callback)(void*)) { app->show_text_input = true; app->text_input_buffer = buffer; app->text_input_buffer_len = buflen; app->text_input_done_callback = done_callback; } -void dismiss_keyboard(ProtoViewApp* app) { +void ui_dismiss_keyboard(ProtoViewApp* app) { view_dispatcher_stop(app->view_dispatcher); } +/* ================================= Alert ================================== */ + +/* Set an alert message to be shown over any currently active view, for + * the specified amount of time of 'ttl' milliseconds. */ +void ui_show_alert(ProtoViewApp* app, const char* text, uint32_t ttl) { + app->alert_dismiss_time = furi_get_tick() + furi_ms_to_ticks(ttl); + snprintf(app->alert_text, ALERT_MAX_LEN, "%s", text); +} + +/* Cancel the alert before its time has elapsed. */ +void ui_dismiss_alert(ProtoViewApp* app) { + app->alert_dismiss_time = 0; +} + +/* Show the alert if an alert is set. This is called after the currently + * active view displayed its stuff, so we overwrite the screen with the + * alert message. */ +void ui_draw_alert_if_needed(Canvas* canvas, ProtoViewApp* app) { + if(app->alert_dismiss_time == 0) { + /* No active alert. */ + return; + } else if(app->alert_dismiss_time < furi_get_tick()) { + /* Alert just expired. */ + ui_dismiss_alert(app); + return; + } + + /* Show the alert. A box with black border and a text inside. */ + canvas_set_font(canvas, FontPrimary); + uint8_t w = canvas_string_width(canvas, app->alert_text); + uint8_t h = 8; // Font height. + uint8_t text_x = 64 - (w / 2); + uint8_t text_y = 32 + 4; + uint8_t padding = 3; + canvas_set_color(canvas, ColorBlack); + canvas_draw_box( + canvas, text_x - padding, text_y - padding - h, w + padding * 2, h + padding * 2); + canvas_set_color(canvas, ColorWhite); + canvas_draw_box( + canvas, + text_x - padding + 1, + text_y - padding - h + 1, + w + padding * 2 - 2, + h + padding * 2 - 2); + canvas_set_color(canvas, ColorBlack); + canvas_draw_str(canvas, text_x, text_y, app->alert_text); +} + /* =========================== Canvas extensions ============================ */ void canvas_draw_str_with_border( diff --git a/applications/plugins/protoview/view_info.c b/applications/plugins/protoview/view_info.c index 9b2c5d044..03a740a63 100644 --- a/applications/plugins/protoview/view_info.c +++ b/applications/plugins/protoview/view_info.c @@ -103,7 +103,7 @@ void render_view_info(Canvas* const canvas, ProtoViewApp* app) { return; } - show_available_subviews(canvas, app, SubViewInfoLast); + ui_show_available_subviews(canvas, app, SubViewInfoLast); switch(app->current_subview[app->current_view]) { case SubViewInfoMain: render_subview_main(canvas, app); @@ -126,7 +126,7 @@ void text_input_done_callback(void* context) { furi_string_free(save_path); free(privdata->filename); - dismiss_keyboard(app); + ui_dismiss_keyboard(app); } /* Replace all the occurrences of character c1 with c2 in the specified @@ -261,9 +261,12 @@ void notify_signal_sent(ProtoViewApp* app) { /* Handle input for the info view. */ void process_input_info(ProtoViewApp* app, InputEvent input) { - if(process_subview_updown(app, input, SubViewInfoLast)) return; + /* If we don't have a decoded signal, we don't allow to go up/down + * in the subviews: they are only useful when a loaded signal. */ + if(app->signal_decoded && ui_process_subview_updown(app, input, SubViewInfoLast)) return; + InfoViewPrivData* privdata = app->view_privdata; - int subview = get_current_subview(app); + int subview = ui_get_current_subview(app); /* Main subview. */ if(subview == SubViewInfoMain) { @@ -280,7 +283,7 @@ void process_input_info(ProtoViewApp* app, InputEvent input) { } else if(input.type == InputTypeLong && input.key == InputKeyOk) { privdata->filename = malloc(SAVE_FILENAME_LEN); set_signal_random_filename(app, privdata->filename, SAVE_FILENAME_LEN); - show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN, text_input_done_callback); + ui_show_keyboard(app, privdata->filename, SAVE_FILENAME_LEN, text_input_done_callback); } else if(input.type == InputTypeShort && input.key == InputKeyOk) { SendSignalCtx send_state; send_signal_init(&send_state, app);