|
| 1 | +import lvgl as lv |
| 2 | + |
| 3 | +import mpos |
| 4 | +from mpos.apps import Activity, Intent |
| 5 | + |
| 6 | +""" |
| 7 | +SettingActivity is used to edit one setting. |
| 8 | +For now, it only supports strings. |
| 9 | +""" |
| 10 | +class SettingActivity(Activity): |
| 11 | + |
| 12 | + active_radio_index = -1 # Track active radio button index |
| 13 | + prefs = None # taken from the intent |
| 14 | + |
| 15 | + # Widgets: |
| 16 | + keyboard = None |
| 17 | + textarea = None |
| 18 | + dropdown = None |
| 19 | + radio_container = None |
| 20 | + |
| 21 | + def __init__(self): |
| 22 | + super().__init__() |
| 23 | + self.setting = None |
| 24 | + |
| 25 | + def onCreate(self): |
| 26 | + self.prefs = self.getIntent().extras.get("prefs") |
| 27 | + setting = self.getIntent().extras.get("setting") |
| 28 | + |
| 29 | + settings_screen_detail = lv.obj() |
| 30 | + settings_screen_detail.set_style_pad_all(mpos.ui.pct_of_display_width(2), 0) |
| 31 | + settings_screen_detail.set_flex_flow(lv.FLEX_FLOW.COLUMN) |
| 32 | + |
| 33 | + top_cont = lv.obj(settings_screen_detail) |
| 34 | + top_cont.set_width(lv.pct(100)) |
| 35 | + top_cont.set_style_border_width(0, 0) |
| 36 | + top_cont.set_height(lv.SIZE_CONTENT) |
| 37 | + top_cont.set_style_pad_all(mpos.ui.pct_of_display_width(1), 0) |
| 38 | + top_cont.set_flex_flow(lv.FLEX_FLOW.ROW) |
| 39 | + top_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0) |
| 40 | + top_cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) |
| 41 | + |
| 42 | + setting_label = lv.label(top_cont) |
| 43 | + setting_label.set_text(setting["title"]) |
| 44 | + setting_label.align(lv.ALIGN.TOP_LEFT,0,0) |
| 45 | + setting_label.set_style_text_font(lv.font_montserrat_20, 0) |
| 46 | + |
| 47 | + ui = setting.get("ui") |
| 48 | + ui_options = setting.get("ui_options") |
| 49 | + current_setting = self.prefs.get_string(setting["key"]) |
| 50 | + if ui and ui == "radiobuttons" and ui_options: |
| 51 | + # Create container for radio buttons |
| 52 | + self.radio_container = lv.obj(settings_screen_detail) |
| 53 | + self.radio_container.set_width(lv.pct(100)) |
| 54 | + self.radio_container.set_height(lv.SIZE_CONTENT) |
| 55 | + self.radio_container.set_flex_flow(lv.FLEX_FLOW.COLUMN) |
| 56 | + self.radio_container.add_event_cb(self.radio_event_handler, lv.EVENT.VALUE_CHANGED, None) |
| 57 | + # Create radio buttons and check the right one |
| 58 | + self.active_radio_index = -1 # none |
| 59 | + for i, (option_text, option_value) in enumerate(ui_options): |
| 60 | + cb = self.create_radio_button(self.radio_container, option_text, i) |
| 61 | + if current_setting == option_value: |
| 62 | + self.active_radio_index = i |
| 63 | + cb.add_state(lv.STATE.CHECKED) |
| 64 | + elif ui and ui == "dropdown" and ui_options: |
| 65 | + self.dropdown = lv.dropdown(settings_screen_detail) |
| 66 | + self.dropdown.set_width(lv.pct(100)) |
| 67 | + options_with_newlines = "" |
| 68 | + for option in ui_options: |
| 69 | + if option[0] != option[1]: |
| 70 | + options_with_newlines += (f"{option[0]} ({option[1]})\n") |
| 71 | + else: # don't show identical options |
| 72 | + options_with_newlines += (f"{option[0]}\n") |
| 73 | + self.dropdown.set_options(options_with_newlines) |
| 74 | + # select the right one: |
| 75 | + for i, (option_text, option_value) in enumerate(ui_options): |
| 76 | + if current_setting == option_value: |
| 77 | + self.dropdown.set_selected(i) |
| 78 | + break # no need to check the rest because only one can be selected |
| 79 | + else: |
| 80 | + # Textarea for other settings |
| 81 | + self.textarea = lv.textarea(settings_screen_detail) |
| 82 | + self.textarea.set_width(lv.pct(100)) |
| 83 | + self.textarea.set_height(lv.SIZE_CONTENT) |
| 84 | + self.textarea.align_to(top_cont, lv.ALIGN.OUT_BOTTOM_MID, 0, 0) |
| 85 | + if current_setting: |
| 86 | + self.textarea.set_text(current_setting) |
| 87 | + placeholder = setting.get("placeholder") |
| 88 | + if placeholder: |
| 89 | + self.textarea.set_placeholder_text(placeholder) |
| 90 | + self.textarea.add_event_cb(lambda *args: mpos.ui.anim.smooth_show(self.keyboard), lv.EVENT.CLICKED, None) # it might be focused, but keyboard hidden (because ready/cancel clicked) |
| 91 | + self.textarea.add_event_cb(lambda *args: mpos.ui.anim.smooth_hide(self.keyboard), lv.EVENT.DEFOCUSED, None) |
| 92 | + # Initialize keyboard (hidden initially) |
| 93 | + self.keyboard = MposKeyboard(settings_screen_detail) |
| 94 | + self.keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) |
| 95 | + self.keyboard.add_flag(lv.obj.FLAG.HIDDEN) |
| 96 | + self.keyboard.add_event_cb(lambda *args: mpos.ui.anim.smooth_hide(self.keyboard), lv.EVENT.READY, None) |
| 97 | + self.keyboard.add_event_cb(lambda *args: mpos.ui.anim.smooth_hide(self.keyboard), lv.EVENT.CANCEL, None) |
| 98 | + self.keyboard.set_textarea(self.textarea) |
| 99 | + |
| 100 | + # Button container |
| 101 | + btn_cont = lv.obj(settings_screen_detail) |
| 102 | + btn_cont.set_width(lv.pct(100)) |
| 103 | + btn_cont.set_style_border_width(0, 0) |
| 104 | + btn_cont.set_height(lv.SIZE_CONTENT) |
| 105 | + btn_cont.set_flex_flow(lv.FLEX_FLOW.ROW) |
| 106 | + btn_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0) |
| 107 | + # Save button |
| 108 | + save_btn = lv.button(btn_cont) |
| 109 | + save_btn.set_size(lv.pct(45), lv.SIZE_CONTENT) |
| 110 | + save_label = lv.label(save_btn) |
| 111 | + save_label.set_text("Save") |
| 112 | + save_label.center() |
| 113 | + save_btn.add_event_cb(lambda e, s=setting: self.save_setting(s), lv.EVENT.CLICKED, None) |
| 114 | + # Cancel button |
| 115 | + cancel_btn = lv.button(btn_cont) |
| 116 | + cancel_btn.set_size(lv.pct(45), lv.SIZE_CONTENT) |
| 117 | + cancel_label = lv.label(cancel_btn) |
| 118 | + cancel_label.set_text("Cancel") |
| 119 | + cancel_label.center() |
| 120 | + cancel_btn.add_event_cb(lambda e: self.finish(), lv.EVENT.CLICKED, None) |
| 121 | + |
| 122 | + if False: # No scan QR button for text settings because they're all short right now |
| 123 | + cambutton = lv.button(settings_screen_detail) |
| 124 | + cambutton.align(lv.ALIGN.BOTTOM_MID,0,0) |
| 125 | + cambutton.set_size(lv.pct(100), lv.pct(30)) |
| 126 | + cambuttonlabel = lv.label(cambutton) |
| 127 | + cambuttonlabel.set_text("Scan data from QR code") |
| 128 | + cambuttonlabel.set_style_text_font(lv.font_montserrat_18, 0) |
| 129 | + cambuttonlabel.align(lv.ALIGN.TOP_MID, 0, 0) |
| 130 | + cambuttonlabel2 = lv.label(cambutton) |
| 131 | + cambuttonlabel2.set_text("Tip: Create your own QR code,\nusing https://genqrcode.com or another tool.") |
| 132 | + cambuttonlabel2.set_style_text_font(lv.font_montserrat_10, 0) |
| 133 | + cambuttonlabel2.align(lv.ALIGN.BOTTOM_MID, 0, 0) |
| 134 | + cambutton.add_event_cb(self.cambutton_cb, lv.EVENT.CLICKED, None) |
| 135 | + |
| 136 | + self.setContentView(settings_screen_detail) |
| 137 | + |
| 138 | + def onStop(self, screen): |
| 139 | + if self.keyboard: |
| 140 | + mpos.ui.anim.smooth_hide(self.keyboard) |
| 141 | + |
| 142 | + def radio_event_handler(self, event): |
| 143 | + print("radio_event_handler called") |
| 144 | + target_obj = event.get_target_obj() |
| 145 | + target_obj_state = target_obj.get_state() |
| 146 | + print(f"target_obj state {target_obj.get_text()} is {target_obj_state}") |
| 147 | + checked = target_obj_state & lv.STATE.CHECKED |
| 148 | + current_checkbox_index = target_obj.get_index() |
| 149 | + print(f"current_checkbox_index: {current_checkbox_index}") |
| 150 | + if not checked: |
| 151 | + if self.active_radio_index == current_checkbox_index: |
| 152 | + print(f"unchecking {current_checkbox_index}") |
| 153 | + self.active_radio_index = -1 # nothing checked |
| 154 | + return |
| 155 | + else: |
| 156 | + if self.active_radio_index >= 0: # is there something to uncheck? |
| 157 | + old_checked = self.radio_container.get_child(self.active_radio_index) |
| 158 | + old_checked.remove_state(lv.STATE.CHECKED) |
| 159 | + self.active_radio_index = current_checkbox_index |
| 160 | + |
| 161 | + def create_radio_button(self, parent, text, index): |
| 162 | + cb = lv.checkbox(parent) |
| 163 | + cb.set_text(text) |
| 164 | + cb.add_flag(lv.obj.FLAG.EVENT_BUBBLE) |
| 165 | + # Add circular style to indicator for radio button appearance |
| 166 | + style_radio = lv.style_t() |
| 167 | + style_radio.init() |
| 168 | + style_radio.set_radius(lv.RADIUS_CIRCLE) |
| 169 | + cb.add_style(style_radio, lv.PART.INDICATOR) |
| 170 | + style_radio_chk = lv.style_t() |
| 171 | + style_radio_chk.init() |
| 172 | + style_radio_chk.set_bg_image_src(None) |
| 173 | + cb.add_style(style_radio_chk, lv.PART.INDICATOR | lv.STATE.CHECKED) |
| 174 | + return cb |
| 175 | + |
| 176 | + def gotqr_result_callback_unused(self, result): |
| 177 | + print(f"QR capture finished, result: {result}") |
| 178 | + if result.get("result_code"): |
| 179 | + data = result.get("data") |
| 180 | + print(f"Setting textarea data: {data}") |
| 181 | + self.textarea.set_text(data) |
| 182 | + |
| 183 | + def cambutton_cb_unused(self, event): |
| 184 | + print("cambutton clicked!") |
| 185 | + self.startActivityForResult(Intent(activity_class=CameraApp).putExtra("scanqr_mode", True), self.gotqr_result_callback) |
| 186 | + |
| 187 | + def save_setting(self, setting): |
| 188 | + ui = setting.get("ui") |
| 189 | + ui_options = setting.get("ui_options") |
| 190 | + if ui and ui == "radiobuttons" and ui_options: |
| 191 | + selected_idx = self.active_radio_index |
| 192 | + new_value = "" |
| 193 | + if selected_idx >= 0: |
| 194 | + new_value = ui_options[selected_idx][1] |
| 195 | + elif ui and ui == "dropdown" and ui_options: |
| 196 | + selected_index = self.dropdown.get_selected() |
| 197 | + print(f"selected item: {selected_index}") |
| 198 | + new_value = ui_options[selected_index][1] |
| 199 | + elif self.textarea: |
| 200 | + new_value = self.textarea.get_text() |
| 201 | + else: |
| 202 | + new_value = "" |
| 203 | + old_value = self.prefs.get_string(setting["key"]) |
| 204 | + |
| 205 | + # Save it |
| 206 | + if setting.get("dont_persist") is not True: |
| 207 | + editor = self.prefs.edit() |
| 208 | + editor.put_string(setting["key"], new_value) |
| 209 | + editor.commit() |
| 210 | + |
| 211 | + # Update model for UI |
| 212 | + setting["value_label"].set_text(new_value if new_value else "(not set)") |
| 213 | + self.finish() # the self.finish (= back action) should happen before callback, in case it happens to start a new activity |
| 214 | + |
| 215 | + # Call changed_callback if set |
| 216 | + changed_callback = setting.get("changed_callback") |
| 217 | + #print(f"changed_callback: {changed_callback}") |
| 218 | + if changed_callback and old_value != new_value: |
| 219 | + print(f"Setting {setting['key']} changed from {old_value} to {new_value}, calling changed_callback...") |
| 220 | + changed_callback(new_value) |
0 commit comments