omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    Welcome!

    This is the community forum for my apps Pythonista and Editorial.

    For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.


    dialogs module. Extending it....

    Pythonista
    dialogs
    3
    9
    7120
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Phuket2
      Phuket2 last edited by

      @omz, I am not sure if you are interested in extending the dialogs module further or not.
      But 2 easy things come to mind.. In the fields dict you could include a list for validation eg. ['Mr', 'Mrs', 'Dr'] etc... With a match case predicate.
      The other one I think about is a list of allowed chars or the inverse of that. So if I had a disallowed list like [" "], would stop the user from entering a space in the field. This would give a one word field. Of course would not stop punctuation etc, but you could also add constants to filter out combinations also.

      Another way and I think might be better is to just add a callback function to the list of types.
      Or callback functions. Like validate, keypress, leaving field etc... Ok, then just delegates I guess. I haven't worked with the text delegates yet.

      The extra mile might be even to have a callback to create the cell and assign a delegate to it.

      I am not sure about your idea with the dialogs. I am trying to write a DialogManager around it. If it stays the same, it's still good. But with a few enhancements I think it could be great

      1 Reply Last reply Reply Quote 0
      • omz
        omz last edited by

        Yeah, some sort of data validation for dialogs.form_dialog() would definitely be useful.

        Here's an idea for a relatively simple implementation that wouldn't require changes in the dialogs module itself (though it duplicates some code from there). It's not as granular as you might want (e.g. it's not possible to validate input while typing), but it's probably good enough for a lot of use cases. It's basically just an additional validator parameter, which should be a function/callable. When the Done button in the dialog is tapped, this function gets the (preliminary) result of the dialog, can inspect it, and it should return a list of fields (keys) that are invalid for whatever reason. These fields are then highlighted in red, and the dialog remains on screen.

        # Extension of dialogs.form_dialog() that supports simple data validation
        
        import dialogs
        import collections
        import ui
        
        class ValidatingFormDialogController (dialogs._FormDialogController):
        	def done_action(self, sender):
        		if callable(self.validator):
        			invalid_keys = self.validator(self.values)
        			if invalid_keys:
        				for i, (title, fields) in enumerate(self.sections):
        					for j, field in enumerate(fields):
        						cell = self.cells[i][j]
        						if field['key'] in invalid_keys:
        							cell.text_label.text_color = 'red'
        						else:
        							cell.text_label.text_color = None
        				return
        		if self.shield_view:
        			self.dismiss_datepicker(None)
        		else:
        			ui.end_editing()
        			self.was_canceled = False
        			self.container_view.close()
        		
        def form_dialog(title='', fields=None, sections=None, done_button_title='Done', validator=None):
        	if not sections and not fields:
        		raise ValueError('sections or fields are required')
        	if not sections:
        		sections = [('', fields)]
        	if not isinstance(title, str) and not isinstance(title, unicode):
        		raise TypeError('title must be a string')
        	for section in sections:
        		if not isinstance(section, collections.Sequence):
        			raise TypeError('Sections must be sequences (title, fields)')
        		if len(section) < 2:
        			raise TypeError('Sections must have 2 or 3 items (title, fields[, footer]')
        		if not isinstance(section[0], str) and not isinstance(section[0], unicode):
        			raise TypeError('Section titles must be strings')
        		if not isinstance(section[1], collections.Sequence):
        			raise TypeError('Expected a sequence of field dicts')
        		for field in section[1]:
        			if not isinstance(field, dict):
        				raise TypeError('fields must be dicts')
        	c = ValidatingFormDialogController(title, sections, done_button_title=done_button_title)
        	c.validator = validator
        	c.container_view.present('sheet')
        	c.container_view.wait_modal()
        	c.container_view = None
        	if c.was_canceled:
        		return None
        	return c.values
        
        # --- DEMO:
        
        def validate_form(values):
        	# This gets called before the dialog is dismissed via 'Done'.
        	# It should return a list of keys that failed to pass validation.
        	# If no invalid keys are returned, the dialog is closed as usual,
        	# otherwise, the invalid fields are highlighted in red.
        	invalid = []
        	# Title must be 'Mr', 'Mrs', or 'Ms':
        	if values.get('title') not in ['Mr', 'Mrs', 'Ms']:
        		invalid.append('title')
        	# Name must not be empty:
        	if len(values.get('name')) < 1:
        		invalid.append('name')
        	# 'Accept Terms' must be checked:
        	if not values.get('terms'):
        		invalid.append('terms')
        	return invalid
        
        def main():
        	fields = [{'type': 'text', 'key': 'title', 'title': 'Title (Mr/Mrs/Ms)'}, {'type': 'text', 'key': 'name', 'title': 'Name'}, {'type': 'switch', 'key': 'terms', 'title': 'Accept Terms'}]
        	r = form_dialog('Test', fields, validator=validate_form)
        	print r
        
        if __name__ == '__main__':
        	main()
        
        Phuket2 2 Replies Last reply Reply Quote 0
        • Phuket2
          Phuket2 last edited by

          @omz, I am a little out of my depth here. But what I think is that with this approach you have ended up opening the whole of the dialogs module or most of it.

          Eg. I can access all the TableViewCells and get to the TextFields from the cells subviews etc. Theoretically allowing me to change the the delegate of the TextField, add items to the cell etc...
          Add another right_buttons_items and so...

          But before I go running off down this path, I wanted to check in with you. maybe my thinking is too simplistic and flawed. maybe there are some gotchas I am not getting.

          I did some simple tests, but basic. Just to make sure I could access what I thought I could.

          1 Reply Last reply Reply Quote 0
          • Phuket2
            Phuket2 @omz last edited by

            @omz ok, here is a super crude update.

            # coding: utf-8
            
            # Extension of dialogs.form_dialog() that supports simple data validation
            
            import dialogs
            import collections
            import ui
            
            class ValidatingFormDialogController (dialogs._FormDialogController):
                def done_action(self, sender):
                    if callable(self.validator):
                        invalid_keys = self.validator(self.values)
                        if invalid_keys:
                            for i, (title, fields) in enumerate(self.sections):
                                for j, field in enumerate(fields):
                                    cell = self.cells[i][j]
                                    if field['key'] in invalid_keys:
                                        cell.text_label.text_color = 'red'
                                    else:
                                        cell.text_label.text_color = None
                            return
                    if self.shield_view:
                        self.dismiss_datepicker(None)
                    else:
                        ui.end_editing()
                        self.was_canceled = False
                        self.container_view.close()
            
            class MyTextFieldDelegate (object):
            	def textfield_should_begin_editing(self, textfield):
            		return True
            	def textfield_did_begin_editing(self, textfield):
            		pass
            	def textfield_did_end_editing(self, textfield):
            		pass
            	def textfield_should_return(self, textfield):
            		textfield.end_editing()
            		return True
            	def textfield_should_change(self, textfield, range, replacement):
            		if replacement == ' ':
            			return False
            		print textfield, range, replacement
            		return True
            	def textfield_did_change(self, textfield):
            		pass
            
                   
            def form_dialog(title='', fields=None, sections=None, done_button_title='Done', validator=None):
                if not sections and not fields:
                    raise ValueError('sections or fields are required')
                if not sections:
                    sections = [('', fields)]
                if not isinstance(title, str) and not isinstance(title, unicode):
                    raise TypeError('title must be a string')
                for section in sections:
                    if not isinstance(section, collections.Sequence):
                        raise TypeError('Sections must be sequences (title, fields)')
                    if len(section) < 2:
                        raise TypeError('Sections must have 2 or 3 items (title, fields[, footer]')
                    if not isinstance(section[0], str) and not isinstance(section[0], unicode):
                        raise TypeError('Section titles must be strings')
                    if not isinstance(section[1], collections.Sequence):
                        raise TypeError('Expected a sequence of field dicts')
                    for field in section[1]:
                        if not isinstance(field, dict):
                            raise TypeError('fields must be dicts')
                c = ValidatingFormDialogController(title, sections, done_button_title=done_button_title)
                cell = c.cells[0][0]
                print c.cells[0][0].text_label.text
                tf = cell.content_view.subviews[0]
                tf.text = 'Hi there'
                tf.clear_button_mode = 'when_editing'
                tf.delegate = MyTextFieldDelegate()
                c.validator = validator
                c.container_view.present('sheet')
                c.container_view.wait_modal()
                c.container_view = None
                if c.was_canceled:
                    return None
                return c.values
            
            # --- DEMO:
            
            def validate_form(values):
                # This gets called before the dialog is dismissed via 'Done'.
                # It should return a list of keys that failed to pass validation.
                # If no invalid keys are returned, the dialog is closed as usual,
                # otherwise, the invalid fields are highlighted in red.
                invalid = []
                # Title must be 'Mr', 'Mrs', or 'Ms':
                if values.get('title') not in ['Mr', 'Mrs', 'Ms']:
                    invalid.append('title')
                # Name must not be empty:
                if len(values.get('name')) < 1:
                    invalid.append('name')
                # 'Accept Terms' must be checked:
                if not values.get('terms'):
                    invalid.append('terms')
                return invalid
            
            def main():
                fields = [{'type': 'text', 'key': 'title', 'title': 'Title (Mr/Mrs/Ms)'}, {'type': 'text', 'key': 'name', 'title': 'Name'}, {'type': 'switch', 'key': 'terms', 'title': 'Accept Terms'}]
                r = form_dialog('Test', fields, validator=validate_form)
                print r
            
            if __name__ == '__main__':
                main()```
            1 Reply Last reply Reply Quote 0
            • Phuket2
              Phuket2 last edited by Phuket2

              As far as I can see, the validation can happen in the delegate, but it's exciting

              1 Reply Last reply Reply Quote 0
              • Phuket2
                Phuket2 @omz last edited by Phuket2

                @omz , no need to waste time answering me. I have done quite a few tests and it works fine. Of course have to be mindful changing text sizes etc. if this is required could copy the code you use to size and position the edit field into a resize function anyway.
                I noticed you are using ui.measure_string. I assume you have fixed this as I remember it was broken in 1.5
                It would be nice if in your next release you could name the TextField used for input. Just would give more certainty when retrieving it.

                But I have to say, what you did is very nice. From a closed system, to a very flexible one. Also very easy to add menu btns with actions etc...

                Edit/addition
                Oh, I will also try emulating your date picker to make custom slide panels. Not sure what to use them for yet. But I am sure, they could be useful

                Anyway, thanks for sharing it....

                1 Reply Last reply Reply Quote 0
                • Phuket2
                  Phuket2 last edited by Phuket2

                  @omz, Is coming along. The view in the pic is a pyui file. Loaded at runtime and with the exact same animations you do for datepicker. Dismisses also correctly as you do with date picker.
                  For the moment, the cancel btn in the menu activates it. Cl8cking the shield dismisses it. Not returning values yet.

                  What I have done so far is very tied into the dialog. I made a class for the slide up panel, but I pass it a reference to the dialog. The easy way first. I am sure I can uncouple it.
                  But, more my point is that this would a great addition to the ui. Look, if I could write it professionally I would. I will write a seperate class sometime for ui, but will still be amateur hour for me.

                  1 Reply Last reply Reply Quote 0
                  • cvp
                    cvp last edited by cvp

                    Hello @Phuket2 , I found your topic by searching validation of a form dialog.
                    I need something similar and I tried the delegate method of the textfield.
                    Checking while typing is ok (in textfield_should_change) but the form_dialog does not return the changed values.
                    Did you success with this code?

                    If I put these two lines after wait_modal,

                        c.container_view.wait_modal()
                        print('tf.text=',tf.text)
                        print('c.values=',c.values)
                    

                    tf.text contains the typed value, but c.values not!?

                    Solved (for easy case of only one section) by:

                    for i in range(0,len(c.cells[0])):			# loop on rows of section 0
                    	cell = c.cells[0][i]					# ui.TableViewCell of row i
                    	tf = cell.content_view.subviews[0] 	# ui.TextField of value in row
                    	tf.delegate = MyTextFieldDelegate()
                    	
                    c.validator = validator
                    	
                    c.container_view.present('sheet')
                    c.container_view.wait_modal()
                    # Get rid of the view to avoid a retain cycle:
                    c.container_view = None
                    if c.was_canceled:
                    	return None
                    		
                    for i in range(0,len(c.cells[0])):			# loop on rows of section 0
                    	cell = c.cells[0][i]					# ui.TableViewCell of row i
                    	tf = cell.content_view.subviews[0] 	# ui.TextField of value in row
                    	c.values[tf.name] = tf.text			# set return values	
                    print('c.values=',c.values)
                    	
                    return c.values
                    
                    Phuket2 1 Reply Last reply Reply Quote 0
                    • Phuket2
                      Phuket2 @cvp last edited by

                      @cvp , sorry I did not get back to you. I have been inundated with visitors. I would not have that been that much help anyway, was so -long ago. But great you solved it.

                      1 Reply Last reply Reply Quote 0
                      • First post
                        Last post
                      Powered by NodeBB Forums | Contributors