90% working GTK2 backend

"[email protected]" <[email protected]> Mon, 30 Dec 2002 00:55:17 -0500
Newsgroups gmane.comp.python.anygui.devel
Message-ID <[email protected]>
Hello,
Here is sort of a Christmas/New Year gift - a 90% functioning
implementation of GTK2 backend based on the new API (the broken version in
the CVS).

The only things missing are the menu support plus combo boxes (though the
genericgui combobox works just fine). 

All tests seem to pass and the layout is correct, unicode is supported too. 

Requires PyGTK 2.0. 

It would be nice if that would be checked in to AnyGui CVS, though code is
little rough around the edges. 

Eugene Pervago
<[email protected]> Ulka Inc.


--------------------------------------------------------------------
mail2web - Check your email from the web at
http://mail2web.com/ .
gtkgui.py (application/octet-stream, 19.8 KB)
"""Test status:

Missing: test_menu

test_aboutdlg OK
test_backend  OK
test_canvas   NA
test_button   OK
test_checkbox OK
test_combobox OK (Not native though)
test_component NA
test_curses OK
test_defaults OK
test_events OK
test_frame  OK
test_interface NA (Others don't pass either)
test_invisible OK
test_label OK
test_layout OK
test_menu
test_modv1 OK
test_modv2 OK
test_modv3 OK
test_openfiledlg OK   (Some bugs on exit)
test_place OK
test_radiobutton NA   (Can't implement, doesn't work like a toggle button)
test_radiogroup  OK
test_references  OK
test_remove   (Works only the first time)
test_rules  NA
test_shake  OK 
test_tags   NA
test_textarea OK
test_textfield OK
test_window    OK

demo/ttt.py OK
demo/ttt2.py NA (Fails on Tk & wx)
demo/ifs.py  NA (Fails on Tk & wx. Canvas missing)
demo/chmodgui.py OK

"""

try:
    # Import Anygui infrastructure. You shouldn't have to change these.
    from anygui.backends import *
    from anygui.Applications import AbstractApplication
    from anygui.Wrappers import AbstractWrapper
    from anygui.Events import *
    from anygui import application
    # End Anygui imports.

    # Import anything needed to access the backend API. This is
    # your job!
    import pygtk
    
    pygtk.require('2.0')
    
    import gtk
    # End backend API imports.
except:
    import traceback
    traceback.print_exc()

__all__ = '''

  Application
  ButtonWrapper
  WindowWrapper
  LabelWrapper
  TextFieldWrapper
  TextAreaWrapper
  ListBoxWrapper
  RadioButtonWrapper
  CheckBoxWrapper
  MenuWrapper
  MenuCommandWrapper
  MenuCheckWrapper
  MenuSeparatorWrapper
  MenuBarWrapper

'''.split()

class ComponentWrapper(AbstractWrapper):

    def __init__(self, *args, **kwds):

        AbstractWrapper.__init__(self, *args, **kwds)

        # 'container' before everything, then geometry.
        self.setConstraints('container','x','y','width','height')

        # addConstraint(self,before,after) adds a constraint that attribute
        # 'before' must be set before 'after', when both appear in the
        # same push operation. An attempt to set mutually contradictory
        # constraints will result in an exception.
        self.addConstraint('geometry', 'visible')

    def widgetFactory(self,*args,**kws):
        raise NotImplementedError, 'should be implemented by subclasses'

    def enterMainLoop(self): # ...
        if not self.widget: return
        self.widget.show()
        self.proxy.push()

    def destroy(self):
        if self.widget:
            self.widget.destroy()
        self.widget = None

    # getters and setters
    def setContainer(self,container):
        if container is None:
            try:
                self.destroy()
            except:
                pass
            return
        parent = container.wrapper
        if parent is not None:
            self.create()
            cont = parent._getContainer()
            self.widget.unparent()
            if hasattr(cont, 'put'):
                cont.put(self.widget, 0, 0)
            elif hasattr(cont, 'add'):
                cont.add(self.widget)
            self.proxy.push(blocked=['container'])

    def create(self):
        if not self.widget:
            self.widget = self.widgetFactory()
            self.widget.visible = self.proxy.state['visible']
            self.widgetSetUp()

    def setGeometry(self,x,y,width,height):
        if not self.widget: return
        if self.widget.parent and hasattr(self.widget.parent, 'move'):
            self.widget.parent.move(self.widget,
                                    int(x), int(y))
        self.widget.set_uposition(int(x), int(y))

        width = max(int(width),   1)
        height = max(int(height), 1)
        
        self.widget.set_size_request(int(width), int(height))
        return        

    def setVisible(self,visible):
        if not self.widget: return
        if visible:
            self.widget.show()
        else:
            self.widget.hide()

    def setEnabled(self,enabled):
        if not self.widget: return
        self.widget.set_sensitive(int(enabled))

    def setText(self,text):
        if not self.widget: return
#        raise NotImplementedError, 'should be implemented by subclasses'

class LabelWrapper(ComponentWrapper):

    def widgetFactory(self, *args, **kws):
        try:
            label = args[0] or ''
        except:
            label = ''
            
        return gtk.Label(label, *args, **kws)

    def setText(self, text):
        if not self.widget: return
        self.widget.set_text(str(text))

class _ComboBoxWrapper(ComponentWrapper):
    def widgetFactory(self, *args, **kws):
        return gtk.Combo()
    def getSelection(self):
        if not self.widget: return
        return int(self.widget.list.selection[0])
    def setSelection(self, selection):
        if not self.widget: return
        self.widget.list.select_row(int(selection), 0)


class ScrollableListBox(gtk.ScrolledWindow):
    """
    A scrollable list box.  Used by ListBoxWrapper.
    """
    def __init__(self, *args, **kw):
        gtk.ScrolledWindow.__init__(self, *args, **kw)
        self._listbox = gtk.CList()
        self._listbox.show()
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.add_with_viewport(self._listbox)

class ListBoxWrapper(ComponentWrapper):

    connected = 0

    def widgetFactory(self, *args, **kws):
        return ScrollableListBox()

    def setItems(self,items):
        """
        Set the contents of the listbox widget. 'items' is a list of
        strings, or of str()-able objects.
        """
        if not self.widget: return
        self.widget._listbox.clear()
        self._items = items[:]
        for item in items:
            self.widget._listbox.append([str(item)])

    def getItems(self):
        """
        Return ths listbox contents, in order, as a list of strings.
        """
        if not self.widget: return
        items = []
        for i in range(self.widget._listbox.rows):
            items.append(self.widget._listbox.get_text(i, 0))        
        return items

    def setSelection(self,selection):
        """
        Set the selection. 'selection' is an integer indicating the
        item to be selected.
        """
        if not self.widget: return
        self.widget._listbox.select_row(int(selection), 0)

    def getSelection(self):
        """
        Return the selected listbox item index as an integer.
        """
        if not self.widget: return
        listbox = self.widget._listbox
        if listbox.selection:
            return listbox.selection[0]
        else:
            return -1

    def widgetSetUp(self):
        """ Connect ListBox events """
        if not self.connected:
            self.widget._listbox.connect("select_row", self._select)
            self.connected = 1

    def _select(self,*args):
        send(self.proxy,'select')

#class CanvasWrapper(ComponentWrapper):
# Fix me!
#    _twclass = tw.Canvas

class ButtonWrapper(ComponentWrapper):
    """
    Wraps a backend command-button widget - the kind you click
    on to initiate an action, eg "OK", "Cancel", etc.
    """
    connected = 0

    def widgetFactory(self, *args, **kws):
        return gtk.Button(*args, **kws)

    def widgetSetUp(self):
        """
        Register a backend event handler to call self.click when
        the user clicks the button.
        """
        if not self.connected:
            self.widget.connect("clicked", self.clickHandler)
            self.connected = 1

    def setText(self, text):
        if not self.widget: return
        
        if self.widget.get_children():
            self.widget.get_children()[0].set_text(str(text))
        else:
            label = gtk.Label(str(text))
            label.show()
            self.widget.add(label)

    def clickHandler(self,*args,**kws):
        send(self.proxy,'click')

class ToggleButtonMixin(ButtonWrapper):
    def widgetFactory(self, *args, **kws):
        return gtk.ToggleButton(*args, **kws)

    def getOn(self):
        """ Return the button's state: 1 for checked, 0 for not. """
        if not self.widget: return
        return self.widget.get_active()

    def widgetSetUp(self):
        ButtonWrapper.widgetSetUp(self)
        self._toggling = 0
        self.widget.connect('toggled', self.toggleHandler)
        
    def setOn(self,on):
        """ Set the button's state. """
        if not self.widget: return
        val = self.widget.get_active()
        if val == int(on):
            return
        
        self._toggling = 1        
        self.widget.set_active(int(on))
        self._toggling = 0

    def toggleHandler(self, data):
        if not self._toggling:
            send(self.proxy, 'click')
    
    def clickHandler(self, data):        
        if not self._toggling:
            send(self.proxy, 'click')
            

class CheckBoxWrapper(ToggleButtonMixin):
    """
    Usually ToggleButtonMixin completely handles the CheckBox
    behavior, but in case it doesn't, you may need to write some
    code here.
    """
    def widgetFactory(self, *args, **kws):
        return gtk.CheckButton(*args, **kws)
    

class RadioButtonWrapper(ToggleButtonMixin):
    """
    Radio buttons are a pain. Only one member of an RB "group" may be
    active at a time. Anygui provides the RadioGroup front-end
    class, which takes care of querying the state of radiobuttons
    and setting their state in a mutually exclusive manner.
    However, many backends also enforce this mutual exclusion,
    which means that things can get complicated. The RadioGroup
    class is implemented in as non-intrusive a way as possible,
    but it can be a challenge to arrange for them to act in
    the correct way, backend-wise. Look at the other backend
    implementations for some clues.

    In the case where a backend implements radiobuttons as a simple
    visual variant of checkboxes with no mutual-exclusion behavior,
    this class already does everything you need; just create the
    proper backend widget in RadioButtonWrapper.widgetFactory(). You
    can usually fake that kind of backend implementation by playing
    tricks with the backend's mutual-exclusion mechanism. For example,
    create a tiny frame to encapsulate the backend radiobutton, if
    your backend enforces mutual exclusion on a per-frame basis.
    """

    def widgetFactory(self, *args, **kws):
        if self.proxy.group and len(self.proxy.group._items) > 1:
            item = None
            for item in self.proxy.group._items:
                if item is not self:
                   break
            else:
                raise InternalError(self, "Couldn't find non-self group item!")
            return gtk.RadioButton(item.wrapper.widget, *args, **kws)
        else:
            return gtk.RadioButton(None, *args, **kws)

    def setGroup(self,group):
        if group is None:
            return
        if self.proxy not in group._items:
            group._items.append(self.proxy)
            
class TextFieldWrapper(ComponentWrapper):
    """
    Wraps a native single-line entry field.
    """
    def setEditable(self,editable):
        """
        Set the editable state of the widget. If 'editable' is 0,
        the widget should allow selection and copying of its text,
        but should not accept user input.
        """
        if not self.widget: return
        self.widget.set_editable(editable)

    def getSelection(self):
        """
        Return the first and last+1 character indexes covered by the
        selection, as a tuple; or (0,0) if there's no selection.
        """
        if not self.widget: return
        bounds = self.widget.get_selection_bounds()
        if bounds:
            return bounds
        else:
            position = self.widget.get_position()
            return (position, position)

    def setSelection(self,selection):
        """
        Select the indicated text. 'selection' is a tuple of two
        integers indicating the first and last+1 indexes within
        the widget text that should be covered by the selection.
        """
        if not self.widget: return
        self.widget.select_region(selection[0], selection[1])

    def setText(self,text):
        """ Set/get the text associated with the widget. This might be
        window title, frame caption, label text, entry field text,
        or whatever.
        """
        if not self.widget: return
        self.widget.delete_text(0, -1)
        self.widget.insert_text(str(text))

    def getText(self):
        """
        Fetch the widget's associated text. You *must* implement this
        to get the text from the native widget; the default getText()
        from ComponentWrapper (almost) certainly won't work.
        """
        if not self.widget: return
        return self.widget.get_chars(0, -1)
    
    def widgetFactory(self, *args, **kws):
        return gtk.Entry(*args, **kws)
    
    def widgetSetUp(self):
        """
        Arrange for a press of the "Enter" key to call self._return.
        """
        pass

    def _return(self,*args,**kws):
        send(self.proxy, 'enterkey')

class ScrollableTextView(gtk.ScrolledWindow):
    """
    A scrollable text view.  Used by TextAreaWrapper.
    """
    def __init__(self, *args, **kw):
        gtk.ScrolledWindow.__init__(self, *args, **kw)        
        self._textview = gtk.TextView()
        self._textview.show()
        self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.add(self._textview)

class TextAreaWrapper(ComponentWrapper):
    """
    Wraps a native multiline text area. If TextControlMixin works
    for your backend, you shouldn't need to change anything here.
    """
    def widgetFactory(self, *args, **kws):
        frame = gtk.Frame()
        scrollable = ScrollableTextView(*args, **kws)
        scrollable.show()
        frame.add(scrollable)
        frame._textview = scrollable._textview
        return frame

    def widgetSetUp(self):
        if self.widget:
            self.widget._textview.set_wrap_mode(gtk.WRAP_WORD)
    
    def getText(self):
        if not self.widget: return
        buffer = self.widget._textview.get_buffer()
        return buffer.get_text(buffer.get_start_iter(),
                               buffer.get_end_iter(),
                               False)

    def setText(self, text):
        if not self.widget: return
        self.widget._textview.get_buffer().set_text(str(text))

    def setSelection(self, selection):
        if not self.widget: return
        start, end = selection
        buffer = self.widget._textview.get_buffer()
        buffer.move_mark_by_name('selection_bound',
                                 buffer.get_iter_at_offset(start))
        buffer.move_mark_by_name('insert',
                                 buffer.get_iter_at_offset(end))

    def getSelection(self):
        return self.widget._textview.get_selection_bounds()

    def setEditable(self, editable):
        if not self.widget: return
        self.widget._textview.set_editable(editable)        


# Incomplete: fix the remainder of this file!

class ContainerMixin:
    """
    Frames - that is, widgets whose job is to visually group
    other widgets - often have a lot of behavior in common
    with top-level windows. Abstract that behavior here.
    """
    pass

class FrameWrapper(ContainerMixin,ComponentWrapper):

    def __init__(self,*args,**kws):
        ComponentWrapper.__init__(self,*args,**kws)

    def widgetFactory(self):
        return gtk.Frame()
    
    def _getContainer(self):
        if not hasattr(self, '_gtk_container'):
            self.create()

        return self._gtk_container

    def widgetSetUp(self):
        if not hasattr(self, '_gtk_container'):
            self._gtk_container = gtk.Layout()
            self.widget.add(self._gtk_container)
            self._gtk_container.show()

    def addToContainer(self,container):
        """
        Add the Frame to its back-end container (another
        Frame or a Window).
        """
        #container.wrapper.widget.add(self.widget)
        raise NotImplementedError, 'should be implemented by subclasses'

    def setContainer(self, *args, **kws):
        """
        OK, this probably needs to be pulled into a mixin heritable by
        various backends.
        
        Ensure all contents are properly created. This looks like it could
        be handled at the Proxy level, but it probably *shouldn't* be -
        it's handling a Tk-specific requirement about the order in which
        widgets must be created. (I did it the Proxy way too. This way
        is definitely "the simplest thing that could possibly work.") - jak
        """
        ComponentWrapper.setContainer(self, *args, **kws)
#        for component in self.proxy.contents:
#            component.container = self.proxy
    
    

class WindowWrapper(ContainerMixin,ComponentWrapper):
    """
    Wraps a top-level window frame.
    """
    connected = 0

    def widgetFactory(self, *args, **kws):
        return gtk.Window(gtk.WINDOW_TOPLEVEL)

    def setTitle(self,title):
        if not self.widget: return
        self.widget.set_title(str(title))

    def _getContainer(self):
        if not hasattr(self, '_gtk_container'):
            self.widgetSetUp()
        return self._gtk_container
    
    def addToContainer(self,container):
        """
        Add self to the backend application, if required.
        """
        raise NotImplementedError, 'should be implemented by subclasses'

    def widgetSetUp(self):
        """
        Arrange for self.resize() to be called whenever the user
        interactively resizes the window.
        """
        if not self.widget:
            self.create()

        if not hasattr(self, '_gtk_container'):
            self._gtk_container = gtk.Layout()
            self.widget.add(self._gtk_container)
            self._gtk_container.show()
        if not self.connected:
            self.widget.connect('size_allocate', self.resizeHandler)
            self.widget.connect('destroy', self.close)
            self.connected = 1

    def close(self, event):
        self.widget.destroy()
        self.destroy()
        application().remove(self.proxy)

    def resizeHandler(self, *args):
        w = self.widget.get_allocation().width
        h = self.widget.get_allocation().height
        dw = w - self.proxy.state['width']
        dh = h - self.proxy.state['height']

        if (dw,dh) == (0,0):
            return        

        #ensure proxy state is updated
        self.proxy.state['height'] = h
        self.proxy.state['width']  = w
        
        self.proxy.resized(dw, dh)
        
    def setContainer(self,container):
        if not application().isRunning(): return
        if container is None: return
        if self.widget is None:
            self.create()
        self.proxy.push(blocked=['container'])
        # Ensure contents are properly created.
        for comp in self.proxy.contents:
            comp.container = self.proxy

    def setGeometry(self, x, y, width, height):
        if not self.widget: return
        self.widget.set_uposition(x, y)
        self.widget.set_default_size(width, height)

class MenuWrapper:
    pass

class MenuCommandWrapper:
    pass

class MenuCheckWrapper:
    pass

class MenuSeparatorWrapper:
    pass

class MenuBarWrapper:
    pass


class Application(AbstractApplication):
    """
    Wraps a backend Application object (or implements one from
    scratch if necessary).

    wxgui's Application class inherits wxPython's Application class.
    On the other hand, Tk has no Application class, so tkgui's
    Application class simply calls Tk.mainloop() in its
    Application.internalRun() method.
    """
    def __init__(self, **kwds):
        AbstractApplication.__init__(self, **kwds)
        self._isRunning = True
        
    def isRunning(self): return self._isRunning
    def internalRemove(self):        
        if not self._windows:
            gtk.main_quit()
            
    def internalRun(self):
        """
        Do whatever is necessary to start your backend's event-
        handling loop.
        """
        gtk.main()