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.
Swipe TableViewCell to show multiple actions
-
Ahh, by printing arguments, I realized that I forgot that blocks have hidden arguments pointing to themselves.
class _block_descriptor (Structure): _fields_ = [('reserved', c_ulong), ('size', c_ulong), ('copy_helper', c_void_p), ('dispose_helper', c_void_p), ('signature', c_char_p)] InvokeFuncType = ctypes.CFUNCTYPE(None, *[c_void_p, ctypes.c_bool]) class _block_literal(Structure): _fields_ = [('isa', c_void_p), ('flags', c_int), ('reserved', c_int), ('invoke', InvokeFuncType), ('descriptor', _block_descriptor)] def handler(_blk, _action, _sourceView, _comp): print(ObjCInstance (_action)) print(ObjCInstance(_sourceView)) print(ObjCInstance(_comp)) blk=_block_literal.from_address(_comp) print(blk.descriptor.signature) blk.invoke(_comp, True) handler_block=ObjCBlock(handler,restype=None,argtypes=[c_void_p, c_void_p,c_void_p, c_void_p])
-
…. But now I get a crash whenever I press enter at the console again. I think something in the VC needs to be retained?
-
@JonB With new code, I get
Traceback (most recent call last): File "_ctypes/callbacks.c", line 234, in 'calling callback function' TypeError: handler() missing 1 required positional argument: '_comp'
-
@JonB my error, I forgot to add a c_void_p
No more segmentation error
Log is
<UIContextualAction: 0x2856e4e60: style=0, title=@Ryubai's' action 😅, backgroundColor=UIExtendedSRGBColorSpace 0 0 1 0.5> <UISwipeActionStandardButton: 0x115808080; frame = (81 0; 120 43.5); anchorPoint = (0, 0.5); opaque = NO; autoresize = W+H; tintColor = UIExtendedGrayColorSpace 1 1; layer = <CALayer: 0x28360d860>> <__NSStackBlock__: 0x16bb0c608> b'5\xf0\xb2\xdc\x01'
-
Strange... I don't get a crash right away, but if I close the VC and type anything in the console, I get a seg fault. I moved everything into the main code, deleted the auto releases, and tried dismissing the VC from within the handler, but same result. Maybe I broke something else elsewhere.
Well ... If it works, good! Note you can use the action argument's title property to figure out which button was tapped, if you don't want to have a different handler for each button.
-
@JonB If I start Pythonista and I run the script a first time, the 2nd button is absent. Then, I close the program and relaunch it, the 2nd button appears. That's also strange
And if I relaunch it a third time, I have a segmentation error
-
@JonB said
Referring back to a time when I was smarter
I can't believe that, smarter than now....Is that really possible 😉
-
This post is deleted! -
One answer might be to simple remove the blk.invoke line.
I will experiment a bit more this weekend
-
Thanks for all the help! And sorry for taking a while to get back to you, I’ve been really busy these past few days. I tried your code and both actions are showing up for me every time, however Pythonista is still crashing from segmentation faults whenever I tap either action (whether or not I remove the blk.invoke). Thanks again and I agree that it’s hard to imagine a time when @JonB was smarter!
-
@colint could you try this little script which does not use ObjectiveC but marvelous gestures module of @mikael
Swipe left a row and test "delete" and "yours edit" buttons
It is not perfect, for instance, you can swipe left several rows, but this could be protected easily by some extra lines of code. Only to show what is possible without ObjectiveC
You could even replace the swipe by a tap or a double tap on the row.
import console import ui import gestures class MyTableView(ui.View): def __init__(self,w,h): self.width = w self.height = h tbl = ui.TableView() tbl.frame = (0,0,w,h) tbl.row_height = 50 tbl.data_source = ui.ListDataSource(items=['a','b','c','d','e']) tbl.delegate = self tbl.data_source.tableview_cell_for_row = self.tableview_cell_for_row tbl.data_source.tableview_can_delete = self.tableview_can_delete tbl.background_color = (0,0,0,0) self.add_subview(tbl) def but_action(self,sender): v = sender.superview tbl = v.tv row = v.row for sv in v.subviews: v.remove_subview(sv) if sender.title == 'delete': b = console.alert('delete row',str(row), 'confirm', 'cancel', hide_cancel_button=True) if b == 1: del tbl.data_source.items[row] tbl.reload() elif sender.title == 'yours edit': t = tbl.data_source.items[row] t = console.input_alert('enter text of row', str(row), t, 'ok', hide_cancel_button=True) tbl.data_source.items[row] = t v.text_label.text = t def LEFT_swipe(self,data): v = data.view w = v.width bdel = ui.Button() bdel.frame = (w,0,w/4,v.height) bdel.background_color = 'red' bdel.tint_color = 'white' bdel.title = 'delete' bdel.action = self.but_action v.add_subview(bdel) byou = ui.Button() byou.frame = (bdel.x+bdel.width,0,w/4,v.height) byou.background_color = 'green' byou.tint_color = 'white' byou.title = 'yours edit' byou.action = self.but_action v.add_subview(byou) def animation(): bdel.x = w/2 byou.x = bdel.x + bdel.width ui.animate(animation, duration=0.3) def RIGHT_swipe(self,data): v = data.view for sv in v.subviews: v.remove_subview(sv) def tableview_cell_for_row(self, tableview, section, row): # Create and return a cell for the given section/row cell = ui.TableViewCell() cell.tv = tableview cell.row = row cell.selectable = False cell.text_label.text = tableview.data_source.items[row] gestures.swipe(cell,self.LEFT_swipe, direction=gestures.LEFT) gestures.swipe(cell,self.RIGHT_swipe, direction=gestures.RIGHT) return cell def tableview_can_delete(self, tableview, section, row): # Return True if the user should be able to delete the given row. return False # to be sure that standard delete button is not displayed def main(): # Hide script w,h = ui.get_screen_size() mi = min(w,h)*0.9 my_back = MyTableView(mi,mi) my_back.background_color='white' my_back.name = 'Test for @colint' my_back.present('sheet',hide_title_bar=False) my_back.wait_modal() # Protect against import if __name__ == '__main__': main()
-
@colint feed-back hoped, please
-
@cvp Thanks for the script, the gestures module seems really neat! I tried out your code and the actions showed up nicely, the only thing is that for some reason, the script freezes when I press either of the buttons. I have had some issues with console.alert that I think started after some iOS update, so it may just be that that is causing the problem.
-
@colint strange, I don't have any freeze. I know that sometimes console.alert needs to be decorated by @ui.in_background, but here, I don't have the problem. Anyway, the challenge was to have several buttons when swiping, isn'it?
-
Strange, it must be just a problem on my end then. When I put ui.inbackground it doesn’t freeze, but the alerts show up behind the presented view, so I can only see them once I close the window. Anyway, like you said, as long as both actions show up then this is a good alternative way to do it! The only thing I’m wondering about now is if there is a way to get the same behaviour from the original solution, where a long swipe to the left would perform one of the actions. Thanks again for the help!
-
@colint said
long swipe
In the gestures module, the swipe function has min_distance and max_distance but they do not seem to work or you can't have two different swipes in the same direction.
Anyway, these parameters seem to belong to a private API, I think, and so it would be not allowed to be used.I try to get the width of the swipe gesture and to process differently for a short or a long swipe but, actually, without success.
Edit : I think that should be possible without using gestures but the standard touch_began/moved/ended methods where you can continuously compare finger movement from its begin to deduce if the swipe is short or long.
I could try but not this Sunday, normally tomorrow -
I could try but not this Sunday, normally tomorrow
Of course, no rush! That’s interesting, I didn’t know you could use touch_began, etc. with the ui module. I suppose it would need a little math to calculate which cell the touch was on but that should still be doable.
-
@colint said
would need a little math to calculate which cell the touch was
Not needed, try this draft of solution, not yet finished, very far of the end, only to let you know how it could work. I did not want to send something so early, only to show something you can try.
Don't describe me a list of bugs, I know 😂import console import ui class cell_view(ui.View): def __init__(self,w,h,tableview,row): self.width = w self.height = h self.tv = tableview self.row = row if self.tv.swiped: prev = self.tv.swiped prev['bdel'].x = self.width prev['byou'].x = prev['bdel'].x + prev['bdel'].width self.tv.swiped = self #self.border_width = 1 #self.border_color = 'red' bdel = ui.Button(name='bdel') bdel.frame = (w-0,0,w/8,self.height) bdel.background_color = 'red' bdel.tint_color = 'white' bdel.title = 'delete' bdel.action = self.but_action self.add_subview(bdel) byou = ui.Button(name='byou') byou.frame = (bdel.x+bdel.width,0,w/8,self.height) byou.background_color = 'green' byou.tint_color = 'white' byou.title = 'your edit' byou.action = self.but_action self.add_subview(byou) def touch_began(self, touch): #print('touch_began') self.x0,self.y0 = touch.location def touch_moved(self, touch): x,y = touch.location if abs(y-self.y0) < 100: # swipe if x < self.x0: # swipe left self['bdel'].x = max(self['bdel'].x + (x-touch.prev_location.x), self.width*3/4) self['byou'].x = self['bdel'].x + self['bdel'].width if self.tv.swiped: prev = self.tv.swiped if prev != self: prev['bdel'].x = self.width prev['byou'].x = prev['bdel'].x + prev['bdel'].width self.tv.swiped = self def touch_ended(self, touch): pass def but_action(self,sender): tbl = self.tv row = self.row self['bdel'].x = self.width self['byou'].x = self['bdel'].x + self['bdel'].width self.tv.swiped = None if sender.title == 'delete': b = console.alert('delete row',str(row), 'confirm', 'cancel', hide_cancel_button=True) if b == 1: del tbl.data_source.items[row] tbl.reload() elif sender.title == 'your edit': t = tbl.data_source.items[row] t = console.input_alert('enter text of row', str(row), t, 'ok', hide_cancel_button=True) tbl.data_source.items[row] = t class MyTableView(ui.View): def __init__(self,w,h): self.width = w self.height = h tbl = ui.TableView() tbl.swiped = None tbl.frame = (0,0,w,h) tbl.row_height = 50 tbl.data_source = ui.ListDataSource(items=['a','b','c','d','e']) tbl.delegate = self tbl.data_source.tableview_cell_for_row = self.tableview_cell_for_row tbl.data_source.tableview_can_delete = self.tableview_can_delete tbl.background_color = (0,0,0,0) self.add_subview(tbl) def tableview_cell_for_row(self, tableview, section, row): # Create and return a cell for the given section/row cell = ui.TableViewCell() cell.selectable = False cell.text_label.text = tableview.data_source.items[row] v = cell_view(self.width,tableview.row_height,tableview,row) cell.content_view.add_subview(v) return cell def tableview_can_delete(self, tableview, section, row): # Return True if the user should be able to delete the given row. return False # to be sure that standard delete button is not displayed def main(): # Hide script w,h = ui.get_screen_size() mi = min(w,h)*0.9 my_back = MyTableView(mi,mi) my_back.background_color='white' my_back.name = 'Test for @colint' my_back.present('sheet',hide_title_bar=False) my_back.wait_modal() # Protect against import if __name__ == '__main__': main()
-
@colint ok, I'll stop with this last code, surely not perfect but, I think, sufficient to show you that you can do it like you asked. Even long swipe (where delete button width is more than half screen) is foreseen and simulates the tap of the delete button. Try the code and give me some feed-back.
I'm sure that you could ameliorate it in a better way than mine,import console import ui class cell_view(ui.View): def __init__(self,w,h,tableview,row): self.width = w self.height = h self.tv = tableview self.row = row if self.tv.swiped: prev = self.tv.swiped prev['bdel'].x = self.width prev['byou'].x = prev['bdel'].x + prev['bdel'].width self.tv.swiped = self #self.border_width = 1 #self.border_color = 'red' bdel = ui.Button(name='bdel') bdel.frame = (w-0,0,w/8,self.height) bdel.background_color = 'red' bdel.tint_color = 'white' bdel.title = 'delete' bdel.action = self.but_action self.add_subview(bdel) byou = ui.Button(name='byou') byou.frame = (bdel.x+bdel.width,0,w/8,self.height) byou.background_color = 'green' byou.tint_color = 'white' byou.title = 'your edit' byou.action = self.but_action self.add_subview(byou) def touch_began(self, touch): #print('touch_began') if self.tv.swiped: prev = self.tv.swiped if prev != self: prev['bdel'].x = self.width prev['byou'].x = prev['bdel'].x + prev['bdel'].width self.tv.swiped = None self.x0,self.y0 = touch.location def touch_moved(self, touch): x,y = touch.location if abs(y-self.y0) < 100: # swipe left or right self['bdel'].x = min(max(self['bdel'].x + 2*(x-touch.prev_location.x), 0), self.width) self['bdel'].width = max(self.width/8,self.width*7/8-self['bdel'].x) #self['bdel'].x = min(max(self['bdel'].x + 2*(x-touch.prev_location.x), self.width*3/4), self.width) self['byou'].x = self['bdel'].x + self['bdel'].width self.tv.swiped = self def touch_ended(self, touch): if self['bdel'].width > self.width/2: # automatic delete self.but_action('simul delete button') def but_action(self,sender): tbl = self.tv row = self.row self['bdel'].x = self.width self['bdel'].width = self.width/8 self['byou'].x = self['bdel'].x + self['bdel'].width self.tv.swiped = None if sender.title == 'delete' or isinstance(sender, str): b = console.alert('delete row',str(row), 'confirm', 'cancel', hide_cancel_button=True) if b == 1: del tbl.data_source.items[row] tbl.reload() elif sender.title == 'your edit': t = tbl.data_source.items[row] t = console.input_alert('enter text of row', str(row), t, 'ok', hide_cancel_button=True) tbl.data_source.items[row] = t class MyTableView(ui.View): def __init__(self,w,h): self.width = w self.height = h tbl = ui.TableView() tbl.swiped = None tbl.frame = (0,0,w,h) tbl.row_height = 50 tbl.data_source = ui.ListDataSource(items=['a','b','c','d','e']) tbl.delegate = self tbl.data_source.tableview_cell_for_row = self.tableview_cell_for_row tbl.data_source.tableview_can_delete = self.tableview_can_delete tbl.background_color = (0,0,0,0) self.add_subview(tbl) def tableview_cell_for_row(self, tableview, section, row): # Create and return a cell for the given section/row cell = ui.TableViewCell() cell.selectable = False cell.text_label.text = tableview.data_source.items[row] v = cell_view(self.width,tableview.row_height,tableview,row) cell.content_view.add_subview(v) return cell def tableview_can_delete(self, tableview, section, row): # Return True if the user should be able to delete the given row. return False # to be sure that standard delete button is not displayed def main(): # Hide script w,h = ui.get_screen_size() mi = min(w,h)*0.9 my_back = MyTableView(mi,mi) my_back.background_color='white' my_back.name = 'Test for @colint' my_back.present('sheet',hide_title_bar=False) my_back.wait_modal() # Protect against import if __name__ == '__main__': main()
-
Looking good! I’m not seeing the “your edit” button sliding out all the way but I’m sure I can figure that out. Thanks so much for your help!