Forum Archive

Spacing of ButtonItem's

marcus67

Is there a way to influence the spacing of ButtonItem's in a title row bar? They seem to be pretty far apart. In the iPhone layout of my app I have four (one left and three right) of them and now there's hardly any room left for the title itself. Thanks a lot!

screenshot

omz

Not really... It's possible to get something similar by using objc_util though. This way, you can create a ButtonItem that uses a custom view instead of an image/title, and you could add multiple buttons with custom spacing/size to that view...

A Button as a custom view of a ButtonItem doesn't behave completely like a normal ButtonItem with an image though. For example, the touch target of regular ButtonItems is much larger (taps don't need to be as precise).

Anyway, here's a little demo of what I mean. The spacing is very tight in this example, but it's easy to change.

import ui
from objc_util import *

v = ui.View(frame=(0, 0, 400, 400), name='Demo')
v.background_color = 'white'

btn_images = [ui.Image.named(n) for n in ['iob:beaker_32', 'iob:beer_32', 'iob:coffee_32']]
btn_container = ui.View(frame=(0, 0, len(btn_images)*32, 44))
for i, img in enumerate(btn_images):
    btn = ui.Button(image=img)
    btn.frame = (i*32, 0, 32, 44)
    btn_container.add_subview(btn)

btn_item = ui.ButtonItem()
btn_item_objc = ObjCInstance(btn_item)
btn_item_objc.customView = ObjCInstance(btn_container)
v.right_button_items = [btn_item]
v.present('sheet')
marcus67

@omz Hi there! Thanks a lot for offering this sample code snippet. I'm currently turning it into a utility class. Unfortunately, there seems to be an issue with the action method of the ButtonItem's: they are never called. I can see the icons being pressed but nothing else happens. Do I need a little more ObjC wizadry for this? Thanks! I appreciate your help!

omz

You'd have to use the action of the individual buttons that are added to the container.

marcus67

@omz That's exactly what I'm using. The methods of the individual buttons are not called, at least, not in my setup. See my gist.

omz

The first problem is that you cannot pass an action as a keyword argument to the ui.Button constructor. It's a bit unfortunate that this is silently ignored instead of raising an exception... but you have to assign the action attribute separately.

Unfortunately, your code will crash after you do so. The reason for this is that the container view (and with it, the buttons) get garbage-collected because there are no references to them anymore after get_condensed_list returns. The underlying (ObjC) views still exist, but the Python objects are gone, which leads to garbage pointers and crashes... In short, you have to keep a reference to the btn_container view somehow. I would suggest that you simply assign it as an attribute of v (something like v.button_container = btn_container). This requires some refactoring of your get_condensed_list method. This should work:

# coding: utf-8
# This file is part of https://github.com/marcus67/rechtschreibung

import ui
from objc_util import *

DEFAULT_X_SPACING = 8
DEFAULT_HEIGHT = 44

class ButtonItemCondenser (object):

  def __init__(self, button_item_list, x_spacing=DEFAULT_X_SPACING):

    self.button_item_list = button_item_list
    self.x_spacing = x_spacing

  def get_condensed_list(self):

    # see https://forum.omz-software.com/topic/2724/spacing-of-buttonitem-s
    i = 0
    x = 0
    btn_container = ui.View(name='test')

    for button_item in self.button_item_list:
      btn = ui.Button(image=button_item.image, action=button_item.action)
      #button_item.action(btn_container)
      width = button_item.image.size[0]
      btn.frame = (x, 0, width, DEFAULT_HEIGHT)
      x = x + width + self.x_spacing
      btn_container.add_subview(btn)
      i = i + 1

    x = x - self.x_spacing
    btn_container.frame = (0, 0, x , DEFAULT_HEIGHT)
    btn_item = ui.ButtonItem()
    btn_item_objc = ObjCInstance(btn_item)
    btn_item_objc.customView = ObjCInstance(btn_container)
    return [btn_item]    

def handle_action(sender):
  #print "handle_action: sender.name=%s" % sender.name  
  print str(sender)

#def handle_action():
#  print "handle_action"

def test():

  icon_names = [ 'iob:beaker_32', 'iob:beer_32', 'iob:bag_32' ]

  button_item_list = map(lambda name : ui.ButtonItem(image=ui.Image.named(name), action=handle_action), icon_names)
  condenser = ButtonItemCondenser(button_item_list)

  v = ui.View(frame=(0, 0, 400, 400), name='Demo')
  v.background_color = 'white'  
  condensed_list = condenser.get_condensed_list()
  normal_item = ui.ButtonItem(image=ui.Image.named('iob:checkmark_32'), action=handle_action)
  condensed_list.append(normal_item)
  v.right_button_items = condensed_list
  v.present('sheet')

if __name__ == '__main__':
  test()
marcus67

@omz Hi there! I've incorporated your changes and created references to all otherwise dangling instances. It still crashes, usually when pressing the third button. See here. Any idea?

marcus67

@JonB Sorry for addressing you directly, but you seem to have an abundance of experience in this area. Do you have any idea what could be wrong in my implementation of @omz's ObjC approach (see my gist link below)? Thanks a lot!

omz

@marcus67 A simple one-line fix would be to add a reference to the condenser object to the view before you present it:

# ...
v.condenser = condenser
v.present('sheet')
# ...

This way, you can make sure that the objects you reference in condenser don't get garbage-collected as long as the view is on screen...

marcus67

@omz Thanks a lot for your help! The latest change did it for me.

marcus67

@omz: That's what the title bar looks like now! :-)

corrected title bar