Search code examples
pythonpygamecontrolscustom-controlsz-order

Z Ordering Controls, collision detection on top and bottom of control


I'm working on creating controls in pygame (button and label are the only ones done, as well as setting a tooltip for the control). I'm drawing the controls in the correct Z order but I'm trying to detect that the mouse is not over another control, so it only activates the one that's visible.

This, somewhat, works if you remove test.set_on_bottom(btnButton7), it will only trigger button7 even if the mouse is over one of the buttons below it, but if you click on one of the buttons below button7 and move the mouse over button7 it still thinks that the original button is being clicked (when it shouldn't)

Been wrapping my brain around this problem for a few days now and I just can't see to figure it out.

(Also, I just thought about it this morning but I should have turned each of the controls into a class instead of an id, so this will get reworked later)

The code is too long to post, here's the pastebin link

All of the processing of setting the states and triggering the messages is done in the process_events method

def process_events(self):
    button = pygame.mouse.get_pressed()
    mouse_pos = pygame.mouse.get_pos()

    for control_id in reversed(self.__z_order):
        state = self.__control_list[control_id]['state']

        if state in ('deleted', 'disabled') or not self.__control_list[control_id]['draw']:
            continue

        if self.__control_list[control_id]['rect'].collidepoint(mouse_pos):
            # left mouse is pressed
            if button[0]:
                # current state of this control is hot and left mouse is pressed on it
                if state == 'hot':
                    for x in self.__z_order[0:control_id - 1]:
                        if self.__control_list[x]['state'] == 'pressed':
                            self.__control_list[x]['state'] = 'normal'
                    can_change = True
                    for x in self.__z_order[control_id + 1:]:
                        if self.__control_list[x]['state'] == 'pressed':
                            can_change = False
                            break
                    # change the state to pressed
                    if can_change:
                        self.__control_list[control_id]['state'] = 'pressed'
                        self.__control_list[control_id]['mouse_pos_lclick'] = None
                        self.__control_list[control_id]['mouse_pos_ldown'] = mouse_pos
                        if self.__control_list[control_id]['on_hover_called']:
                            self.__draw_tip = None

                        if (time.clock() - self.__control_list[control_id][
                            'dbl_timer'] >= self.__dbl_click_delay) and \
                                (time.clock() - self.__control_list[control_id]['timer'] <= self.__dbl_click_speed):
                            if self.__event_mode:
                                if self.__control_list[control_id]['on_dbl_lclick']:
                                    self.__control_list[control_id]['on_dbl_lclick']()
                            else:
                                self.__messages.append(self.Message(control_id, PGC_LBUTTONDBLCLK))
                                # print('Double click', self.__control_list[control_id]['text'])
                            self.__control_list[control_id]['dbl_timer'] = time.clock()
                            self.__control_list[control_id]['timer'] = -1
                        break
    # go through the controls from top to bottom first
    for control_id in reversed(self.__z_order):
        state = self.__control_list[control_id]['state']

        if state in ('deleted', 'disabled') or not self.__control_list[control_id]['draw']:
            continue

        # check if the mouse is over this control
        if self.__control_list[control_id]['rect'].collidepoint(mouse_pos):
            # left mouse is not down
            if not button[0]:
                # state is currently pressed
                if state == 'pressed':
                    # check if there's a timer initiated for this control
                    # this prevents 2 clicks + a double click message
                    if self.__control_list[control_id]['timer'] >= 0:
                        self.__control_list[control_id]['dbl_timer'] = -1
                        self.__control_list[control_id]['timer'] = time.clock()
                        # if the event mode
                        if self.__event_mode:
                            # call the function if there is one
                            if self.__control_list[control_id]['on_lclick']:
                                self.__control_list[control_id]['on_lclick']()
                        else:
                            # post the message to the messages queue
                            self.__messages.append(self.Message(control_id, PGC_LBUTTONUP))
                            # print('Click', self.__control_list[control_id]['text'])
                    # the timer is < 0 (should be -1), double click just happened
                    else:
                        # reset the timer to 0 so clicking can happen again
                        self.__control_list[control_id]['timer'] = 0
                    # go through all of the ids below this control
                    for x in self.__z_order[0:control_id - 1]:
                        # set all the hot controls to normal
                        if self.__control_list[x]['state'] == 'hot':
                            self.__control_list[x]['state'] = 'normal'
                    can_change = True
                    # go through all the controls on top of this control
                    for x in self.__z_order[control_id + 1:]:
                        # something else is on top of this and it's already hot, can't change this control
                        if self.__control_list[x]['state'] == 'hot':
                            can_change = False
                            break
                    if can_change:
                        self.__control_list[control_id]['state'] = 'hot'
                        self.__control_list[control_id]['mouse_pos_lclick'] = mouse_pos
                        self.__control_list[control_id]['mouse_pos_ldown'] = None

                # state is not currently hot (but we're hovering over this control)
                elif state != 'hot':
                    # check for any other contorls
                    for x in self.__z_order[0:control_id - 1]:
                        if self.__control_list[x]['state'] == 'hot':
                            self.__control_list[x]['state'] = 'normal'
                    can_change = True
                    for x in self.__z_order[control_id + 1:]:
                        if self.__control_list[x]['state'] == 'hot':
                            can_change = False
                            break
                    # change the state to hot
                    if can_change:
                        self.__control_list[control_id]['state'] = 'hot'
                        self.__control_list[control_id]['mouse_pos_hover'] = mouse_pos
                        # used to start a tooltip (needs work)
                        self.__control_list[control_id]['mouse_pos_rect'] = pygame.Rect(mouse_pos[0] - 7,
                                                                                        mouse_pos[1] - 7,
                                                                                        mouse_pos[0] + 7,
                                                                                        mouse_pos[1] + 7)
                # state is currently 'hot'
                else:
                    # timer for on_hover hasn't been initialized
                    if self.__control_list[control_id]['timer_on_hover'] == 0:
                        self.__control_list[control_id]['timer_on_hover'] = time.clock()
                    # mouse is in the area
                    if self.__control_list[control_id]['mouse_pos_rect'].collidepoint(mouse_pos):
                        # if the on_hover hasn't been triggered and there is a timer for the on_hover
                        if not self.__control_list[control_id]['on_hover_called'] and self.__control_list[control_id]['timer_on_hover']:
                            # if the mouse has been in the hover area for 1.5 seconds or more
                            if time.clock() - self.__control_list[control_id]['timer_on_hover'] >= 1.5:
                                # trigger the hover
                                self.__control_list[control_id]['on_hover_called'] = True
                                # on_hover is a function call, call the function
                                if self.__control_list[control_id]['on_hover']['type'] == 'function':
                                    self.__control_list[control_id]['on_hover']['func'](self.__control_list[control_id]['on_hover']['args'])
                                # on_hover is a tip, set the self.__draw_tip variable to the tip we need
                                else:
                                    self.__draw_tip = self.__control_list[control_id]['on_hover'].copy()
                                    self.__draw_tip['rect'].x = mouse_pos[0]
                                    self.__draw_tip['rect'].y = mouse_pos[1]

        # mouse is not in the control rect and the state is not currently normal
        elif state != 'normal':
            # set it to normal
            self.__control_list[control_id]['state'] = 'normal'
            # clear the on_hover stuff
            if self.__control_list[control_id]['on_hover_called']:
                self.__control_list[control_id]['on_hover_called'] = False
                self.__draw_tip = None
            if self.__control_list[control_id]['timer_on_hover']:
                self.__control_list[control_id]['timer_on_hover'] = 0

Solution

  • (Will select as the correct answer tomorrow when the 24hr timeout is over)

    Ended up figuring it out. I was way over complicating it.

    1. Go through the list of controls in reverse, the first one that the .collidepoint succeeded on, save that control and break the loop
    2. Go through all of the controls and one that's not disabled and isn't currently normal, set to normal
    3. Work with the control that has been saved as needed

      def process_events(self):
          button = pygame.mouse.get_pressed()
          mouse_pos = pygame.mouse.get_pos()
          top_id = -1
      
          for control_id in reversed(self.__z_order):
              if self.__control_list[control_id]['state'] != 'disabled' and \
                      self.__control_list[control_id]['draw'] and \
                      self.__control_list[control_id]['rect'].collidepoint(mouse_pos):
                  top_id = control_id
                  break
      
          if top_id != -1:
              # go through all of the controls
              for control_id in self.__z_order:
                  # skip the top most control and any that are disabled/deleted
                  if self.__control_list[control_id]['state'] != 'disabled' and \
                                  self.__control_list[control_id]['state'] != 'normal' and \
                                  control_id != top_id:
                      # set it to normal
                      self.__control_list[control_id]['state'] = 'normal'
                      # clear the on_hover stuff
                      if self.__control_list[control_id]['on_hover_called']:
                          self.__control_list[control_id]['on_hover_called'] = False
                          self.__draw_tip = None
                      if self.__control_list[control_id]['timer_on_hover']:
                          self.__control_list[control_id]['timer_on_hover'] = 0
          else:
              for control_id in self.__z_order:
                  if self.__control_list[control_id]['state'] != 'disabled':
                      # set it to normal
                      self.__control_list[control_id]['state'] = 'normal'
                      # clear the on_hover stuff
                      if self.__control_list[control_id]['on_hover_called']:
                          self.__control_list[control_id]['on_hover_called'] = False
                          self.__draw_tip = None
                      if self.__control_list[control_id]['timer_on_hover']:
                          self.__control_list[control_id]['timer_on_hover'] = 0
              return
      
          if button[0]:
              # current state of this control is hot and left mouse is pressed on it
              if self.__control_list[top_id]['state'] == 'hot':
                  self.__control_list[top_id]['state'] = 'pressed'
                  self.__control_list[top_id]['mouse_pos_lclick'] = None
                  self.__control_list[top_id]['mouse_pos_ldown'] = mouse_pos
                  if self.__control_list[top_id]['on_hover_called']:
                      self.__draw_tip = None
      
                  if (time.clock() - self.__control_list[top_id][
                      'dbl_timer'] >= self.__dbl_click_delay) and \
                          (time.clock() - self.__control_list[top_id]['timer'] <= self.__dbl_click_speed):
                      if self.__event_mode:
                          if self.__control_list[top_id]['on_dbl_lclick']:
                              self.__control_list[top_id]['on_dbl_lclick']()
                      else:
                          self.__messages.append(self.Message(top_id, PGC_LBUTTONDBLCLK))
                          # print('Double click', self.__control_list[top_id]['text'])
                      self.__control_list[top_id]['dbl_timer'] = time.clock()
                      self.__control_list[top_id]['timer'] = -1
          elif not button[0]:
              # state is currently pressed
              if self.__control_list[top_id]['state'] == 'pressed':
                  # check if there's a timer initiated for this control
                  # this prevents 2 clicks + a double click message
                  if self.__control_list[top_id]['timer'] >= 0:
                      self.__control_list[top_id]['dbl_timer'] = -1
                      self.__control_list[top_id]['timer'] = time.clock()
                      # if the event mode
                      if self.__event_mode:
                          # call the function if there is one
                          if self.__control_list[top_id]['on_lclick']:
                              self.__control_list[top_id]['on_lclick']()
                      else:
                          # post the message to the messages queue
                          self.__messages.append(self.Message(top_id, PGC_LBUTTONUP))
                          # print('Click', self.__control_list[top_id]['text'])
                  # the timer is < 0 (should be -1), double click just happened
                  else:
                      # reset the timer to 0 so clicking can happen again
                      self.__control_list[top_id]['timer'] = 0
                  # go through all of the ids below this control
                  for x in self.__z_order[0:top_id - 1]:
                      # set all the hot controls to normal
                      if self.__control_list[x]['state'] == 'hot':
                          self.__control_list[x]['state'] = 'normal'
                  can_change = True
                  # go through all the controls on top of this control
                  for x in self.__z_order[top_id + 1:]:
                      # something else is on top of this and it's already hot, can't change this control
                      if self.__control_list[x]['state'] == 'hot':
                          can_change = False
                          break
                  if can_change:
                      self.__control_list[top_id]['state'] = 'hot'
                      self.__control_list[top_id]['mouse_pos_lclick'] = mouse_pos
                      self.__control_list[top_id]['mouse_pos_ldown'] = None
      
              # state is not currently hot (but we're hovering over this control)
              elif self.__control_list[top_id]['state'] != 'hot':
                  self.__control_list[top_id]['state'] = 'hot'
                  self.__control_list[top_id]['mouse_pos_hover'] = mouse_pos
                  # used to start a tooltip (needs work)
                  self.__control_list[top_id]['mouse_pos_rect'] = pygame.Rect(mouse_pos[0] - 7,
                                                                              mouse_pos[1] - 7,
                                                                              mouse_pos[0] + 7,
                                                                              mouse_pos[1] + 7)
              # state is currently 'hot'
              else:
                  # timer for on_hover hasn't been initialized
                  if self.__control_list[top_id]['timer_on_hover'] == 0:
                      self.__control_list[top_id]['timer_on_hover'] = time.clock()
                  # mouse is in the area
                  if self.__control_list[top_id]['mouse_pos_rect'].collidepoint(mouse_pos):
                      # if the on_hover hasn't been triggered and there is a timer for the on_hover
                      if not self.__control_list[top_id]['on_hover_called'] and \
                              self.__control_list[top_id]['timer_on_hover']:
                          # if the mouse has been in the hover area for 1.5 seconds or more
                          if time.clock() - self.__control_list[top_id]['timer_on_hover'] >= 1.5:
                              # trigger the hover
                              self.__control_list[top_id]['on_hover_called'] = True
                              # on_hover is a function call, call the function
                              if self.__control_list[top_id]['on_hover']['type'] == 'function':
                                  self.__control_list[top_id]['on_hover']['func'] \
                                      (self.__control_list[top_id]['on_hover']['args'])
                              # on_hover is a tip, set the self.__draw_tip variable to the tip we need
                              else:
                                  self.__draw_tip = self.__control_list[top_id]['on_hover'].copy()
                                  self.__draw_tip['rect'].x = mouse_pos[0]
                                  self.__draw_tip['rect'].y = mouse_pos[1]