Hi all!

I needed a vertical scrollbar, and here it is, together with a scrollbar-enabled
listbox. I'm not sure whether my code is that optimal, but it works. :)

A disadvantage is the fact that, if you drag the handle of the scrollbar with
the
mouse, you only see the effect when releasing the mouse. That's caused by
urwid's way of handling mouse events. Well, I can live with it quite well.


Yours,
Rebecca

#! /usr/bin/env python

"""
A scrollbar for urwid.

Author: Rebecca Breu ([EMAIL PROTECTED])
License: GPL
"""

import math;

import urwid;
import urwid.util;
import urwid.raw_display;
import urwid.canvas;


######################################################################
#Some stuff for old Python:

try:
    sum([1]);  #Since 2.3
except NameError:
    
    def sum(sequence):
        def add(x, y):
            return x + y
        
        return reduce(add, sequence, 0)


try:
    True, False; #Since 2.3
except NameError:
    False = (1==2);
    True = (1==1);


try:
    enumerate([]);  #Since 2.3
    
except NameError:
    
    def enumerate(llist):
        """
        This is only for lists!
        """
        i = 0;
        enum = [];
        for entry in llist:
            enum.append((i, entry))
            i += 1;

        return enum;

    
######################################################################
def hamilton_allocation(counts, alloc):
    """
    Implements the Hamilton Method (Largest remainder method) to calculate
    integral ratios. (Like it is used in some elections.)

    counts -- list of integers ('votes per party')
    alloc -- total amount to be allocated ('total amount of seats')
    """

    total_counts = sum(counts);
    quotas = [float(count)*alloc/total_counts for count in counts]

    fracts = []
    for (i, fp) in enumerate(quotas):
        fracts.append((math.modf(fp)[0], i))
        
    fracts.sort()
    fracts.reverse()

    results = [int(math.modf(quota)[1]) for quota in quotas]    
    remainder = alloc - sum(results)

    for i in range(remainder):
        results[fracts[i][1]] += 1;
            
    return results        


######################################################################
class ScrollBar(urwid.BoxWidget):
    """
    A scrollbar. 
    """
    
    def __init__(self, handle, background, middle, top, bottom):
        """
        handle -- (character, attribute) for the drawing of the scrollbar's
                  handle
        background -- (character, attribute) for the drawing of the background
        middle -- number of visible rows of the corresponding widget
        top -- number of lines above visible rows of the corresponding widget
        bottom -- number of lines below visible rows of the corresponding
                  widget
        """

        handle_canvas = urwid.Text(handle[0]).render((1,))
        self.handle_char = handle_canvas.text[0];
        self.handle_attr = handle[1];
        self.handle_charset = handle_canvas.cs[0][0][0];

        background_canvas = urwid.Text(background[0]).render((1,))
        self.background_char = background_canvas.text[0];
        self.background_attr = background[1];
        self.background_charset = background_canvas.cs[0][0][0];

        self.middle = middle;
        self.top = top;
        self.bottom = bottom;

        self.handle_grab_pos = -1;
        self.handle_moved = 0;


    def selectable(self):
        "Not selectable."""
        return False;


    def mouse_event(self, (maxcol, maxrow), event, button, col, row, focus):
        """
        Handle mouse events.
        """

        (middle, top, bottom) = hamilton_allocation([self.middle, self.top,
                                                     self.bottom], maxrow);

        if (event == "mouse press" and button == 1 and row >= top and
            row < middle + top):

            #User grabs handle with left mouse button
            self.handle_grab_pos = row;
            return True;
        
        elif self.handle_grab_pos >= 0 and event == "mouse release":
            #User releases handle
            self.handle_moved += row - self.handle_grab_pos;
            self.handle_grab_pos = -1;
            return True;

        else:
            return False;
        
        
    def render(self, (maxcol, maxrow), focus=False):
        """
        Render the ScrollBar as a canvas and return it.
        """

        (middle, top, bottom) = hamilton_allocation([self.middle, self.top,
                                                     self.bottom], maxrow);

        text_list = [self.background_char] * top + \
                    [self.handle_char] * middle + \
                    [self.background_char] * bottom;

        #Length of chars maybe > 1 for escape sequences
        handle_rl = len(self.handle_char);
        background_rl = len(self.background_char);

        attr_list = [[(self.background_attr, background_rl)] \
                     for i in range(top)] + \
                    [[(self.handle_attr, handle_rl)] \
                     for i in range(middle)] + \
                    [[(self.background_attr, background_rl)] \
                     for i in range(bottom)]

        charset_list = [[(self.background_charset,  background_rl)] \
                        for i in range(top)] + \
                       [[(self.handle_charset, handle_rl)] \
                        for i in range(middle)] + \
                       [[(self.background_charset,  background_rl)] \
                        for i in range(bottom)]

            
        return urwid.Canvas(text_list, attr_list, charset_list)


    def read_move(self, maxrow, length):
        """
        Updates the position of the scrollbar and returns the amount of lines
        the corresponding widget has to be scrolled. 

        maxrow -- number of displayed rows of the widget to which the
                  scrollbar belongs
        length -- overall length of the widgets list of that widget

        """

        moved = int(float(self.handle_moved)/self.middle * length);
        self.handle_moved = 0;

        #Ensure that we stay in a correct range:
        if self.top + moved < 0:
            moved -= self.top + moved;
        
        if self.bottom - moved < 0:
            moved += self.bottom - moved;
        
        self.top += moved;
        self.bottom -= moved;

        return moved;
    

######################################################################
class SBListBox(urwid.WidgetWrap):
    """
    A ListBox with a scroll bar.
    """

    def __init__(self, body, handle, background):
        """
        body -- list or a SimpleListWalker object that contains the
                widgets to be displayed inside the list box. 
                The __len__ attribute must be implemented!
        handle -- (character, attribute) for the drawing of the scrollbar's
                  handle
        background -- (character, attribute) for the drawing of the scrollbar's
                      background
        """

        self.length = len(body);
        self.listbox = urwid.ListBox(body);
        self.scrollbar = ScrollBar(handle, background, 1, 0, 0);
        self.columns = urwid.Columns([("weight", 1, self.listbox),
                                      ("fixed", 1, self.scrollbar)]);
        
        urwid.WidgetWrap.__init__(self, self.columns);


    def calc_sb_midtopbot(self, size):
        """
        Calculate middle, top, bottom for the scrollbar.
        """
        
        (middle, top, bottom) = self.listbox.calculate_visible(size);

        self.scrollbar.middle = min(size[1], self.length);
        self.scrollbar.top = middle[2] - len(top[1]);
        self.scrollbar.bottom = self.length - middle[2] - len(bottom[1]) - 1;
        

    def render(self, size, focus=False):
        """
        Render the SBListBox as a canvas and return it.
        """
        
        self.calc_sb_midtopbot(size);
        return self.columns.render(size, focus);


    def mouse_event(self, size, event, button, col, row, focus):
        """
        Handle mouse events.
        """
        
        if event == "mouse release" and self.scrollbar.handle_grab_pos >= 0:
            #If the handle is grabbed, pretend that every release is over the
            #scrollbar:
            col = size[0] - 1;
            
        handled = self.columns.mouse_event(size, event, button, col, row,
                                           focus);

        #Scroll the listbox according to handle movement:
        moved = self.scrollbar.read_move(size[1], self.length);
        
        position = self.listbox.get_focus()[1];
        (offset, inset) = self.listbox.get_focus_offset_inset(size);
        
        assert (position + moved >= 0) or (position + moved <= self.length), \
               "Move out of list range: %i\n" % (position + moved)
        
        self.listbox.change_focus(size, position + moved, offset)


        return handled;
        

######################################################################
def run():
    global ui;

    ui.set_mouse_tracking()
    dim = ui.get_cols_rows();

    wlist = [urwid.AttrWrap(urwid.Edit(str(i)), None, "focused")
             for i in range(50)]
    listbox = SBListBox(wlist, (" ", "handle"), (u"\u2592", "scrollbar_bg"));
        
    ui.draw_screen(dim, listbox.render(dim, 1))

    keys = True;

    #Main event loop:
    while True:
        if keys:
            ui.draw_screen(dim, listbox.render(dim, 1))
           
        keys = ui.get_input()

        for k in keys:
            if urwid.is_mouse_event(k):
                event, button, col, row = k
                listbox.mouse_event(dim, event, button, col, row, focus=True)
            else:
                listbox.keypress(dim, k);
        
        if "window resize" in keys:
            dim = ui.get_cols_rows();
              
            
######################################################################
#Main part                                                           #
######################################################################
def main():
    global ui;
    ui = urwid.raw_display.Screen();

    ui.register_palette(
        [('focused', 'light red', 'default', 'standout'),
         ('handle', 'light cyan', 'dark cyan', 'standout'),
         ('scrollbar_bg', 'light gray', 'black', 'standout')]);
    
    ui.run_wrapper(run)


if __name__ == "__main__":
    main()
_______________________________________________
Urwid mailing list
[email protected]
http://lists.excess.org/mailman/listinfo/urwid

Reply via email to