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.


    Joystick UIControl ui wrapper?

    Pythonista
    ui module objcutil
    3
    22
    8463
    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.
    • mcriley821
      mcriley821 last edited by

      Inspired by @DoinStuffMobile request for a Joystick-like control for the scene module, I created a very basic joystick using the objc_util module. I’ve gotten it working to use custom actions, pass a few kwargs in, and such things without crashing.

      I was curious as to if there’s a way to wrap the objc_instance of the Joystick with the ui module similar to how @omz has with the other ui views?

      Also, I need suggestions! What should I change to be more efficient, elegant, pythonic, etc? What are some things you guys think I should add?

      Code is kinda long:

      
      from objc_util import *
      import ui
      
      class Joystick (ui.View):
      	def __new__(cls,**kwargs):
      		
      		inst=create_objc_class(
      			'UIJoystick',
      			ObjCClass('UIControl'),
      			methods=[Joystick.beginTrackingWithTouch_withEvent_,Joystick.continueTrackingWithTouch_withEvent_,Joystick.endTrackingWithTouch_withEvent_,Joystick.action]
      			).new()
      		Joystick.__init__(inst,**kwargs)
      		return inst
      			
      	def __init__(self,**kwargs):
      		for key,val in kwargs.items():
      			if key =='action':
      				self.customAction=val
      			elif eval(f'self.{key}()').__class__ != self.aabbbbbbb.__class__:
      				key=key[0].upper()+key[1:]
      				if key == 'Frame':
      					val=CGRect(CGPoint(val[0],val[1]),CGSize(val[2],val[3]))
      				elif key == 'BackgroundColor':
      					val=ObjCClass('UIColor').alloc().initWithRed_green_blue_alpha_(val[0],val[1],val[2],val[3])
      				eval(f'self.set{key}_(val)')
      		#can add more kwargs
      				
      		self.radius=self.size().height/2
      		self._setCornerRadius_(self.radius)
      		stick=ui.View(bg_color='#acacac')
      		stick.touch_enabled=False
      		stick.frame=(
      			0,
      			0,
      			self.size().width,
      			self.size().height)
      		self.originalStickCenter=stick.center
      		stick.corner_radius=self.size().width/2
      		self.addSubview_(stick.objc_instance)
      		self.layer().setBorderColor_(ObjCClass('UIColor').alloc().initWithRed_green_blue_alpha_(0.67, 0.67, 0.67,1.0).CGColor)
      		self.layer().setBorderWidth_(1)
      		return
      	
      	@staticmethod
      	def beginTrackingWithTouch_withEvent_(_self,_cmd,touch,event):
      		#called when a touch enters 
      		#control's bounds
      		return True
      	
      	@staticmethod	
      	def continueTrackingWithTouch_withEvent_(_self,_cmd,touch,event):
      		#called when a touch event is 
      		#updated. like dragging
      		#add code
      		self=ObjCInstance(_self)
      		touchLoc=ObjCInstance(touch).preciseLocationInView_(self)
      		stick=self.subviews()[0]
      		touchVec=CGVector(
      			touchLoc.x-self.radius,
      			touchLoc.y-self.radius)
      		touchVecMagn=(touchVec.dx**2 + touchVec.dy**2)**.5
      		unitTouchVec=CGVector(
      			touchVec.dx/touchVecMagn,
      			touchVec.dy/touchVecMagn)
      		if touchVecMagn < self.radius and touchLoc.x < self.size().width and touchLoc.y < self.size().height:
      			newLoc=touchLoc
      		else:
      			newLoc=CGPoint(
      				self.radius*unitTouchVec.dx+self.radius,
      				self.radius*unitTouchVec.dy+self.radius)
      		stick.setCenter_(newLoc)
      		self.touchVec=touchVec
      		self.sendAction_to_forEvent_('action',self,None)
      		return True
      		
      	@staticmethod
      	def endTrackingWithTouch_withEvent_(_self,_cmd,touch,event):
      		#called when touch ends
      		#add code
      		touch=ObjCInstance(touch)
      		self=ObjCInstance(_self)
      		stick=self.subviews()[0]
      		stick.setCenter_(CGPoint(self.originalStickCenter.x,self.originalStickCenter.y))
      		return True
      	
      	@staticmethod	
      	def action(_self,_cmd):
      		self=ObjCInstance(_self)
      		self.customAction.__call__(ObjCInstance(_self))
      		return
      	
      if __name__=='__main__':
      	x,y=ui.get_screen_size()
      	root=ui.View(
      		frame=(0,0,x,y),
      		bg_color='white')
      	
      	def closeRoot(sender):
      		root.close()
      		return
      		
      	closeButton=ui.Button(
      		frame=(50,75,24,24),
      		image=ui.Image('iob:close_24'),
      		tint_color='#ff0000',
      		action=closeRoot
      		)
      		
      	def moveButton(sender):
      		btn=sender.superview().subviews()[1]
      		btn.setCenter_(CGPoint(btn.center().x+sender.touchVec.dx//10,btn.center().y+sender.touchVec.dy//10))
      		return
      		
      	j=Joystick(
      		frame=(x//2-125,y//2-200,250,250),
      		action=moveButton
      		)
      
      	root.objc_instance.addSubview_(j)
      	root.add_subview(closeButton)
      	root.present('popover',hide_title_bar=True)
      
      1 Reply Last reply Reply Quote 0
      • DoinStuffMobile
        DoinStuffMobile last edited by

        Dude... you're amazing. I need to learn to figure things out like this! I'll definitely be tinkering with how to use and understand this :D

        1 Reply Last reply Reply Quote 1
        • mcriley821
          mcriley821 last edited by mcriley821

          @DoinStuffMobile glad you like it 😂 the only kwargs it can take right now are frame, backgroundColor, and action, so be wary of that.
          I’m working to get a few more things for it to recognize, but the wrapper (hopefully) will handle most of those.

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

            @DoinStuffMobile

            Turns out I like doing stuff the hard way, and all the capabilities to make a joystick already exist in the ui module. I’ve redone the code, and now it’s fully wrapped by the ui module and a ton easier to understand.

            import ui
            
            class Joystick (ui.View):
            	def __init__(self,**kwargs):
            		self.action=None
            		self.stick=ui.View(name='stick')
            		self._tint_color=None
            		for key,val in kwargs.items():
            			if key=='frame' and val[2]!=val[3]:
            				raise ValueError('Joystick must be square')
            			setattr(self,key,val)
            		if not self.bg_color:
            			self.bg_color='#ffffff'	
            		if not self.tint_color:
            			self.tint_color='grey'
            			
            		self.radius=self.width/2
            		self.corner_radius=self.radius
            		self.stick.corner_radius=self.corner_radius
            		self.stick.touch_enabled=False
            		self.stick.frame=(0,0,self.width,self.height)
            		self.originalPosition=self.stick.center
            		self.objc_instance.setClipsToBounds_(False)
            		self.add_subview(self.stick)
            		return
            		
            	def touch_moved(self,touch):
            		touch=touch.location
            		touchVec=ui.Vector2(
            			touch.x-self.radius,
            			touch.y-self.radius)
            		vecMagn=(touchVec.x**2+touchVec.y**2)**.5
            		if vecMagn < self.radius and touch.x < self.width and touch.y < self.height:
            			newLoc=touch
            		else:
            			unitVec=ui.Vector2(
            				touchVec.x/vecMagn,
            				touchVec.y/vecMagn)
            			newLoc=ui.Point(
            				self.radius*unitVec.x+self.radius,
            				self.radius*unitVec.y+self.radius)
            		self.stick.center=newLoc.as_tuple()
            		if self.action:
            			self.action.__call__(self,touchVec)
            		return
            	
            	def touch_ended(self,touch):
            		self.stick.center=self.originalPosition
            		return
            	
            	@property
            	def tint_color(self):
            		return self._tint_color
            			
            	@tint_color.setter
            	def tint_color(self,value):
            		self._tint_color=value
            		self.stick.bg_color=value
            		return
            		
            
            if __name__=='__main__'	:
            	x,y=ui.get_screen_size()
            		
            	j=Joystick(name='joystick',tint_color='red')
            	j.frame=(x//2-50,y//2-150,100,100)
            
            	root=ui.View(frame=(0,0,x,y))
            	root.bg_color='white'
            	
            	def closeRoot(sender):
            		sender.superview.close()
            		return
            	
            	btn=ui.Button(name='button')
            	btn.frame=(50,50,25,25)
            	btn.image=ui.Image('iob:close_32')
            	btn.tint_color='red'
            	btn.action=closeRoot
            	
            	label=ui.Label(name='label')
            	label.frame=(x-175,50,200,20)
            	label.text='x,y'
            	
            	def moveButton(self,touchVec):
            		btn=self.superview.subviews[1]
            		label=self.superview.subviews[2]
            		btn.center=(
            			btn.center.x+touchVec.x/10,
            			btn.center.y+touchVec.y/10)
            		xstr=str(btn.center.x).split('.')
            		ystr=str(btn.center.y).split('.')
            		xstr=xstr[0]+'.'+xstr[1][:4]
            		ystr=ystr[0]+'.'+ystr[1][:4]
            		label.text=f'{xstr},{ystr}'
            		return
            		
            	j.action=moveButton
            	
            	root.add_subview(j)
            	root.add_subview(btn)
            	root.add_subview(label)
            	root.present('popover',hide_title_bar=True) 
            
            mikael DoinStuffMobile 3 Replies Last reply Reply Quote 0
            • mikael
              mikael @mcriley821 last edited by

              @mcriley821, you must be the first who discovered and used objc_util before the ui module. 😁

              One pythonic hint is just the styling for readability. If you select ”Check Style” in the tools (”wrench”) menu, you can see there is quite a lot it does not like. Luckily, there is also ”Reformat Code” available – just have ”Apply Style Guide” and PEP8 selected, and you not have to worry about style.

              1 Reply Last reply Reply Quote 1
              • mikael
                mikael @mcriley821 last edited by

                @mcriley821, shorter way to format floats:

                label_text = f'{btn.center.x:.4f}, {btn.center.y:.4f}'
                
                mcriley821 1 Reply Last reply Reply Quote 1
                • mcriley821
                  mcriley821 @mikael last edited by mcriley821

                  @mikael said:

                  shorter way to format floats:
                  
                  label_text = f'{btn.center.x:.4f}, {btn.center.y:.4f}'
                  

                  This is amazing😂😂😂 i can’t tell you how many times I’ve done string slices. No more!

                  Also, does pythonic just mean style? I always thought pythonic meant being ‘elegant’ (if that makes sense) and efficient. As you can tell from my code, I’m not big on the style lmao. I just want to make sure it’s efficient (time complexity).

                  mikael 6 Replies Last reply Reply Quote 0
                  • mikael
                    mikael @mcriley821 last edited by

                    @mcriley821, style is one part of being elegant. Elegant code is understandable and readable, i.e. not wasting the reader’s time.

                    No need to use __call__, just call it:

                    self.action(self, touchVec)
                    
                    1 Reply Last reply Reply Quote 1
                    • mikael
                      mikael @mcriley821 last edited by

                      @mcriley821, no need to manually calculate vector magnitude:

                      vecMagn = (touchVec.x**2 + touchVec.y**2)**.5
                      

                      ... Vector2 has you covered:

                      vecMagn = abs(touchVec)
                      
                      1 Reply Last reply Reply Quote 0
                      • mikael
                        mikael @mcriley821 last edited by

                        @mcriley821, no need to spend effort trying to make sure the coordinates are integers:

                        j.frame = (x // 2 - 50, y // 2 - 150, 100, 100)
                        

                        Coordinates are floats, and the decimals are even somewhat meaningful on Retina displays.

                        1 Reply Last reply Reply Quote 0
                        • mikael
                          mikael @mcriley821 last edited by

                          @mcriley821, also no need to navigate to btn and label in moveButton, the variables are already available (in the function’s closure).

                          1 Reply Last reply Reply Quote 1
                          • mikael
                            mikael @mcriley821 last edited by

                            @mcriley821, instead of:

                            btn.center = (btn.center.x + touchVec.x / 10,
                                                  btn.center.y + touchVec.y / 10)
                            

                            ... you should be able to just say:

                            btn.center += touchVec / 10
                            

                            ... but for some reason it does not seem to exactly work.

                            1 Reply Last reply Reply Quote 0
                            • mikael
                              mikael @mcriley821 last edited by

                              @mcriley821, ui.View conveniently sets all its kwargs, so we can just call super() with them, no need to iterate manually. Also, defaults can be set before. tint_color we handle separately, as it is a custom property. (In general I would advice against overriding the ui.View properties, it will come back to haunt you.)

                                  def __init__(self, tint_color='grey', **kwargs):
                                      kwargs.setdefault('background_color', 'black')
                                      super().__init__(**kwargs)
                                      ...
                              
                              mcriley821 1 Reply Last reply Reply Quote 0
                              • mcriley821
                                mcriley821 @mikael last edited by mcriley821

                                @mikael I iterate manually to catch a non-square frame. Is there a different way to make sure this happens? I feel like I also need to override the frame setter to catch it after init

                                mikael 3 Replies Last reply Reply Quote 0
                                • mikael
                                  mikael @mcriley821 last edited by mikael

                                  @mcriley821, sorry, forgot to include that bit. Seems enough to check it after super(). If you want to enforce it continuously, layout() is a good option since, in addition frame, size can change via bounds, width and height.

                                      def __init__(self, tint_color='grey', **kwargs):
                                          kwargs.setdefault('background_color', 'black')
                                          super().__init__(**kwargs)
                                          if self.frame.width != self.frame.height:
                                              raise ValueError('Joystick must be square')
                                          ...
                                  
                                  1 Reply Last reply Reply Quote 0
                                  • mikael
                                    mikael @mcriley821 last edited by

                                    @mcriley821, also makes sense to set self.stick.flex = 'WH', so that it will resize with its superview.

                                    1 Reply Last reply Reply Quote 0
                                    • mikael
                                      mikael @mcriley821 last edited by mikael

                                      @mcriley821, taking the arithmetic capabilities of ui.Point/Vector2, the calculations can collapse to:

                                      
                                          def touch_moved(self, touch):
                                              touch = touch.location
                                              radius_vector = [self.radius]*2
                                              touch_vector = touch - radius_vector
                                              magnitude = abs(touch_vector)
                                              
                                              if magnitude < self.radius:
                                                  self.stick.center = touch
                                              else:
                                                  self.stick.center = (
                                                      touch_vector / magnitude *  self.radius + 
                                                      radius_vector
                                                  )
                                              if self.action:
                                                  self.action(self, touch_vector)
                                              return
                                      

                                      Thank you for your patience, I think I am done.

                                      1 Reply Last reply Reply Quote 1
                                      • mcriley821
                                        mcriley821 last edited by

                                        @mikael I really appreciate you taking the time to help me and the improvements! I’ve already updated a bunch of stuff from your suggestions and I’m currently working on using the ‘iob:pinpoint' ionicon to add a type of texture to the ‘joystick’!

                                        mikael 2 Replies Last reply Reply Quote 0
                                        • mikael
                                          mikael @mcriley821 last edited by

                                          @mcriley821, interesting! Careful placement to get just the head of the pin?

                                          1 Reply Last reply Reply Quote 0
                                          • mikael
                                            mikael @mcriley821 last edited by mikael

                                            @mcriley821, if you need a bit more flexibility, here’s an objc CAGradientLayer version, credits for an original axial version to @cvp:

                                            import ui
                                            import objc_util
                                            
                                            
                                            CAGradientLayer = objc_util.ObjCClass('CAGradientLayer')
                                            
                                            
                                            class ShadedCircle(ui.View):
                                                
                                                def __init__(self, color1='#ff8484', color2='red', **kwargs):
                                                    super().__init__(**kwargs)
                                            
                                                    o_color1 = objc_util.UIColor.colorWithRed_green_blue_alpha_(
                                                        *ui.parse_color(color1)).CGColor()
                                                    o_color2 = objc_util.UIColor.colorWithRed_green_blue_alpha_(
                                                        *ui.parse_color(color2)).CGColor()
                                                    
                                                    layer = self.objc_instance.layer()
                                                    grlayer = CAGradientLayer.layer()
                                                    grlayer.setType('radial')
                                                    grlayer.frame = layer.bounds()
                                                    grlayer.setColors_([o_color1, o_color2])
                                                    layer.insertSublayer_atIndex_(grlayer, 0)
                                                    grlayer.setStartPoint(objc_util.CGPoint(0.25, 0.25))
                                                    grlayer.setEndPoint(objc_util.CGPoint(1.0, 1.0))
                                                    grlayer.locations = [0.0, 1.0]
                                                
                                                def layout(self):
                                                    self.height = self.width
                                                    self.corner_radius = self.width / 2
                                                    
                                                
                                            if __name__ == '__main__':
                                                v = ui.View()
                                                
                                                c = ShadedCircle(
                                                    center=v.bounds.center(),
                                                    flex='RTBL',
                                                )
                                                
                                                v.add_subview(c)
                                                v.present('fullscreen', animated=False)
                                            
                                            1 Reply Last reply Reply Quote 1
                                            • First post
                                              Last post
                                            Powered by NodeBB Forums | Contributors