Пример #1
0
class TextEditor(gtk.TextView):
    def __init__(self, textbuffer = None, *args, **kwargs):
        if textbuffer is None:
            textbuffer = TextBuffer()
        gtk.TextView.__init__(self, textbuffer, *args, **kwargs)
        self.buffer           = textbuffer
        self.anno_width       = 200
        self.anno_padding     = 10
        self.anno_layout      = Layout(self)
        self.anno_views       = {}
        self.show_annotations = True
        self.handle_links     = True
        self.set_right_margin(50 + self.anno_padding)
        self.connect('map-event',           self._on_map_event)
        self.connect('expose-event',        self._on_expose_event)
        self.connect('motion-notify-event', self._on_motion_notify_event)
        self.connect('event-after',         self._on_event_after)
        self.buffer.connect('mark-set',           self._on_buffer_mark_set)
        self.buffer.connect('annotation-added',   self._on_annotation_added)
        self.buffer.connect('annotation-removed', self._on_annotation_removed)
        self.set_wrap_mode(gtk.WRAP_WORD)


    def _on_motion_notify_event(self, editor, event):
        if not self.handle_links:
            return
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x),
                                            int(event.y))
        tags = self.get_iter_at_location(x, y).get_tags()

        # Without this call, further motion notify events don't get
        # triggered.
        self.window.get_pointer()

        # If any of the tags are links, show a hand.
        cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
        for tag in tags:
            if tag.get_data('link'):
                cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
                break

        self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor)
        return False


    def _on_event_after(self, textview, event):
        # Handle links here. Only when a button was released.
        if event.type != gtk.gdk.BUTTON_RELEASE:
            return False
        if event.button != 1:
            return False
        if not self.handle_links:
            return

        # Don't follow a link if the user has selected something.
        bounds = self.buffer.get_selection_bounds()
        if bounds:
            start, end = bounds
            if start.get_offset() != end.get_offset():
                return False

        # Check whether the cursor is pointing at a link.
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x),
                                            int(event.y))
        iter = textview.get_iter_at_location(x, y)

        for tag in iter.get_tags():
            link = tag.get_data('link')
            if not link:
                continue
            self.emit('link-clicked', link)
            break
        return False


    def _on_buffer_mark_set(self, buffer, iter, mark):
        self._update_annotations()


    def _on_map_event(self, widget, event):
        self._update_annotation_area()
        self._update_annotations()


    def _on_expose_event(self, widget, event):
        text_window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
        if event.window != text_window:
            return

        # Create the cairo context.
        ctx = event.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        ctx.rectangle(event.area.x,
                      event.area.y,
                      event.area.width,
                      event.area.height)
        ctx.clip()

        self.draw(ctx, *event.window.get_size())


    def draw(self, ctx, w, h):
        if ctx is None:
            return
        if not self.show_annotations:
            return

        # Draw the dashes that connect annotations to their marker.
        ctx.set_line_width(1)
        ctx.set_dash((3, 2))
        right_margin = self.get_right_margin()
        for annotation in self.anno_layout.get_children():
            mark_x, mark_y = self._get_annotation_mark_position(annotation)
            anno_x, anno_y, anno_w, anno_h, d = annotation.window.get_geometry()
            path = [(mark_x,           mark_y - 5),
                    (mark_x,           mark_y),
                    (w - right_margin, mark_y),
                    (w,                anno_y + anno_h / 2)]

            stroke_color = annotation.get_border_color()
            ctx.set_source_rgba(*color.to_rgba(stroke_color))
            ctx.move_to(*path[0])
            for x, y in path[1:]:
                ctx.line_to(x, y)
            ctx.stroke()


    def _get_annotation_mark_offset(self, view1, view2):
        mark1 = view1.annotation.start_mark
        mark2 = view2.annotation.start_mark
        iter1 = self.get_buffer().get_iter_at_mark(mark1)
        iter2 = self.get_buffer().get_iter_at_mark(mark2)
        rect1 = self.get_iter_location(iter1)
        rect2 = self.get_iter_location(iter2)
        if rect1.y != rect2.y:
            return rect1.y - rect2.y
        return rect2.x - rect1.x


    def _get_annotation_mark_position(self, view):
        start_mark     = view.annotation.start_mark
        iter           = self.get_buffer().get_iter_at_mark(start_mark)
        rect           = self.get_iter_location(iter)
        mark_x, mark_y = rect.x, rect.y + rect.height
        return self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT,
                                            mark_x, mark_y)


    def _update_annotation(self, annotation):
        # Resize the annotation.
        item_width = self.anno_width - 2 * self.anno_padding
        annotation.set_size_request(item_width, -1)

        # Find the x, y of the annotation's mark.
        mark_x, mark_y = self._get_annotation_mark_position(annotation)
        self.anno_layout.pull(annotation, mark_y)


    def _update_annotation_area(self):
        # Update the width and color of the annotation area.
        self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, self.anno_width)
        bg_color = self.get_style().base[gtk.STATE_NORMAL]
        window   = self.get_window(gtk.TEXT_WINDOW_RIGHT)
        if window:
            window.set_background(bg_color)


    def _update_annotations(self):
        if not self.show_annotations:
            return

        # Sort the annotations by line/char number and update them.
        iter   = self.get_buffer().get_end_iter()
        rect   = self.get_iter_location(iter)
        height = rect.y + rect.height
        self.anno_layout.sort(self._get_annotation_mark_offset)
        for annotation in self.anno_layout.get_children():
            self._update_annotation(annotation)
        self.anno_layout.update(self.anno_width, height)

        # Update lines.
        self.queue_draw()


    def set_annotation_area_width(self, width, padding = 10):
        self.anno_width   = width
        self.anno_padding = padding
        self._update_annotations()


    def _on_annotation_added(self, buffer, annotation):
        if self.show_annotations == False:
            return
        annotation.set_display_buffer(self.buffer)
        view = AnnotationView(annotation)
        self.anno_views[annotation] = view
        for event in ('focus-in-event', 'focus-out-event'):
            view.connect(event, self._on_annotation_event, annotation, event)
        view.show_all()
        self._update_annotation_area()
        self.anno_layout.add(view)
        self.add_child_in_window(view,
                                 gtk.TEXT_WINDOW_RIGHT,
                                 self.anno_padding,
                                 0)
        self._update_annotations()


    def _on_annotation_removed(self, buffer, annotation):
        view = self.anno_views[annotation]
        self.anno_layout.remove(view)
        self.remove(view)


    def _on_annotation_event(self, buffer, *args):
        annotation = args[-2]
        event_name = args[-1]
        self.emit('annotation-' + event_name, annotation)


    def set_show_annotations(self, active = True):
        if self.show_annotations == active:
            return

        # Unfortunately gtk.TextView deletes all children from the
        # border window if its size is 0. So we must re-add them when the
        # window reappears.
        self.show_annotations = active
        if active:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_added(self.buffer, annotation)
        else:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_removed(self.buffer, annotation)
            self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, 0)


    def set_handle_links(self, handle):
        """
        Defines whether the cursor is changed and whether clicks are accepted
        when hovering over text with tags that have data named "link"
        attached.
        """
        self.handle_links = handle
Пример #2
0
class Table(Element):
    name     = 'table'
    caption  = 'Table'
    xoptions = gtk.EXPAND|gtk.FILL
    yoptions = gtk.FILL

    def __init__(self, rows = 2, cols = 2):
        Element.__init__(self, gtk.Table(rows, cols))
        self.layout     = Layout(0, 0)
        self.entry_rows = None
        self.entry_cols = None
        self.targets    = {}
        self.child.set_row_spacings(3)
        self.child.set_col_spacings(3)
        self._resize_table(rows, cols)


    def has_layout(self):
        return True


    def copy(self):
        widget = Table(self.layout.n_rows, self.layout.n_cols)
        for child in self.child.get_children():
            top   = self.child.child_get_property(child, 'top-attach')
            bot   = self.child.child_get_property(child, 'bottom-attach')
            lft   = self.child.child_get_property(child, 'left-attach')
            rgt   = self.child.child_get_property(child, 'right-attach')
            child = child.copy()
            widget.child.attach(child, lft, rgt, top, bot)
            widget.layout.add(child, lft, rgt, top, bot)
        return widget


    def reassign(self, widget, upper_left, lower_right):
        top = self.child.child_get_property(upper_left,  'top-attach')
        bot = self.child.child_get_property(lower_right, 'bottom-attach')
        lft = self.child.child_get_property(upper_left,  'left-attach')
        rgt = self.child.child_get_property(lower_right, 'right-attach')

        if top >= bot or lft >= rgt:
            return

        # Remove the old target.
        target = widget.get_parent_target()
        widget.unparent()
        self._remove_target(target)

        # Re-add it into the new cells.
        target = self._add_target(top, bot, lft, rgt)
        target.attach(widget)


    def _remove_target(self, target):
        self.layout.remove(target)
        self.child.remove(target)


    def _remove_targets_at(self, top, bot, lft, rgt):
        for row in range(top, bot):
            for col in range(lft, rgt):
                target = self.layout.get_widget_at(row, col)
                self.layout.remove(target)
                if target and target.parent:
                    self.child.remove(target)


    def _add_target(self, top, bot, lft, rgt):
        # Remove the old target, if any.
        self._remove_targets_at(top, bot, lft, rgt)

        # Create a new target.
        target = Target()
        target.connect('child-attached', self._on_target_child_attached)
        target.connect('child-removed',  self._on_target_child_removed)
        target.connect('child-replaced', self._on_target_child_replaced)
        target.set_data('row', top)
        target.set_data('col', lft)
        self.layout.add(target, lft, rgt, top, bot)
        self.child.attach(target, lft, rgt, top, bot)
        target.show_all()
        return target


    def _position_of(self, child):
        top = self.child.child_get_property(child, 'top-attach')
        bot = self.child.child_get_property(child, 'bottom-attach')
        lft = self.child.child_get_property(child, 'left-attach')
        rgt = self.child.child_get_property(child, 'right-attach')
        return lft, rgt, top, bot


    def _resize_table(self, rows, cols):
        old_rows, old_cols = self.layout.n_rows, self.layout.n_cols
        self.layout.resize(rows, cols)

        # Remove useless targets.
        for child in self.child.get_children():
            if child.get_data('row') >= rows or child.get_data('col') >= cols:
                self.child.remove(child)

        # Add new targets.
        self.child.resize(rows, cols)
        for row in range(old_rows, rows):
            for col in range(0, old_cols):
                self._add_target(row, row + 1, col, col + 1)
        for col in range(old_cols, cols):
            for row in range(0, rows):
                self._add_target(row, row + 1, col, col + 1)


    def _child_at(self, x, y):
        for child in self.child.get_children():
            child_x, child_y = self.translate_coordinates(child, x, y)
            if child_x < 0 or child_y < 0:
                continue
            alloc = child.get_allocation()
            if child_x > alloc.width or child_y > alloc.height:
                continue
            return child
        return None


    def target_at(self, x, y):
        child = self._child_at(x, y)
        if child is None:
            return None
        child_x, child_y = self.translate_coordinates(child, x, y)
        target           = child.target_at(child_x, child_y)
        if target is not None:
            return target
        return child


    def _on_target_child_attached(self, target, child):
        self.child.child_set_property(target, 'x-options', child.xoptions)
        self.child.child_set_property(target, 'y-options', child.yoptions)


    def _on_target_child_removed(self, target, child):
        lft, rgt, top, bot = self._position_of(target)

        # Remove the target, as it may have a cellspan.
        self.child.remove(target)

        # Add new targets into each cell.
        for row in range(top, bot):
            for col in range(lft, rgt):
                self._add_target(row, row + 1, col, col + 1)


    def _on_target_child_replaced(self, target, child):
        self._on_target_child_attached(target, child)
Пример #3
0
class Table(Element):
    name = 'table'
    caption = 'Table'
    xoptions = gtk.EXPAND | gtk.FILL
    yoptions = gtk.FILL

    def __init__(self, rows=2, cols=2):
        Element.__init__(self, gtk.Table(rows, cols))
        self.layout = Layout(0, 0)
        self.entry_rows = None
        self.entry_cols = None
        self.targets = {}
        self.child.set_row_spacings(3)
        self.child.set_col_spacings(3)
        self._resize_table(rows, cols)

    def has_layout(self):
        return True

    def copy(self):
        widget = Table(self.layout.n_rows, self.layout.n_cols)
        for child in self.child.get_children():
            top = self.child.child_get_property(child, 'top-attach')
            bot = self.child.child_get_property(child, 'bottom-attach')
            lft = self.child.child_get_property(child, 'left-attach')
            rgt = self.child.child_get_property(child, 'right-attach')
            child = child.copy()
            widget.child.attach(child, lft, rgt, top, bot)
            widget.layout.add(child, lft, rgt, top, bot)
        return widget

    def reassign(self, widget, upper_left, lower_right):
        top = self.child.child_get_property(upper_left, 'top-attach')
        bot = self.child.child_get_property(lower_right, 'bottom-attach')
        lft = self.child.child_get_property(upper_left, 'left-attach')
        rgt = self.child.child_get_property(lower_right, 'right-attach')

        if top >= bot or lft >= rgt:
            return

        # Remove the old target.
        target = widget.get_parent_target()
        widget.unparent()
        self._remove_target(target)

        # Re-add it into the new cells.
        target = self._add_target(top, bot, lft, rgt)
        target.attach(widget)

    def _remove_target(self, target):
        self.layout.remove(target)
        self.child.remove(target)

    def _remove_targets_at(self, top, bot, lft, rgt):
        for row in range(top, bot):
            for col in range(lft, rgt):
                target = self.layout.get_widget_at(row, col)
                self.layout.remove(target)
                if target and target.parent:
                    self.child.remove(target)

    def _add_target(self, top, bot, lft, rgt):
        # Remove the old target, if any.
        self._remove_targets_at(top, bot, lft, rgt)

        # Create a new target.
        target = Target()
        target.connect('child-attached', self._on_target_child_attached)
        target.connect('child-dropped', self._on_target_child_removed)
        target.connect('child-replaced', self._on_target_child_replaced)
        target.set_data('row', top)
        target.set_data('col', lft)
        self.layout.add(target, lft, rgt, top, bot)
        self.child.attach(target, lft, rgt, top, bot)
        target.show_all()
        return target

    def _position_of(self, child):
        top = self.child.child_get_property(child, 'top-attach')
        bot = self.child.child_get_property(child, 'bottom-attach')
        lft = self.child.child_get_property(child, 'left-attach')
        rgt = self.child.child_get_property(child, 'right-attach')
        return lft, rgt, top, bot

    def _resize_table(self, rows, cols):
        old_rows, old_cols = self.layout.n_rows, self.layout.n_cols
        self.layout.resize(rows, cols)

        # Remove useless targets.
        for child in self.child.get_children():
            if child.get_data('row') >= rows or child.get_data('col') >= cols:
                self.child.remove(child)

        # Add new targets.
        self.child.resize(rows, cols)
        for row in range(old_rows, rows):
            for col in range(0, old_cols):
                self._add_target(row, row + 1, col, col + 1)
        for col in range(old_cols, cols):
            for row in range(0, rows):
                self._add_target(row, row + 1, col, col + 1)

    def _child_at(self, x, y):
        for child in self.child.get_children():
            child_x, child_y = self.translate_coordinates(child, x, y)
            if child_x < 0 or child_y < 0:
                continue
            alloc = child.get_allocation()
            if child_x > alloc.width or child_y > alloc.height:
                continue
            return child
        return None

    def target_at(self, x, y):
        child = self._child_at(x, y)
        if child is None:
            return None
        child_x, child_y = self.translate_coordinates(child, x, y)
        target = child.target_at(child_x, child_y)
        if target is not None:
            return target
        return child

    def _on_target_child_attached(self, target, child):
        self.child.child_set_property(target, 'x-options', child.xoptions)
        self.child.child_set_property(target, 'y-options', child.yoptions)

    def _on_target_child_removed(self, target, child):
        lft, rgt, top, bot = self._position_of(target)

        # Remove the target, as it may have a cellspan.
        self.child.remove(target)

        # Add new targets into each cell.
        for row in range(top, bot):
            for col in range(lft, rgt):
                self._add_target(row, row + 1, col, col + 1)

    def _on_target_child_replaced(self, target, child):
        self._on_target_child_attached(target, child)
Пример #4
0
class TextEditor(gtk.TextView):
    def __init__(self, textbuffer=None, *args, **kwargs):
        if textbuffer is None:
            textbuffer = TextBuffer()
        gtk.TextView.__init__(self, textbuffer, *args, **kwargs)
        self.buffer = textbuffer
        self.anno_width = 200
        self.anno_padding = 10
        self.anno_layout = Layout(self)
        self.anno_views = {}
        self.show_annotations = True
        self.handle_links = True
        self.set_right_margin(50 + self.anno_padding)
        self.connect('map-event', self._on_map_event)
        self.connect('expose-event', self._on_expose_event)
        self.connect('motion-notify-event', self._on_motion_notify_event)
        self.connect('event-after', self._on_event_after)
        self.buffer.connect('mark-set', self._on_buffer_mark_set)
        self.buffer.connect('annotation-added', self._on_annotation_added)
        self.buffer.connect('annotation-removed', self._on_annotation_removed)
        self.set_wrap_mode(gtk.WRAP_WORD)

    def _on_motion_notify_event(self, editor, event):
        if not self.handle_links:
            return
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x), int(event.y))
        tags = self.get_iter_at_location(x, y).get_tags()

        # Without this call, further motion notify events don't get
        # triggered.
        self.window.get_pointer()

        # If any of the tags are links, show a hand.
        cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
        for tag in tags:
            if tag.get_data('link'):
                cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
                break

        self.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(cursor)
        return False

    def _on_event_after(self, textview, event):
        # Handle links here. Only when a button was released.
        if event.type != gtk.gdk.BUTTON_RELEASE:
            return False
        if event.button != 1:
            return False
        if not self.handle_links:
            return

        # Don't follow a link if the user has selected something.
        bounds = self.buffer.get_selection_bounds()
        if bounds:
            start, end = bounds
            if start.get_offset() != end.get_offset():
                return False

        # Check whether the cursor is pointing at a link.
        x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_WIDGET,
                                            int(event.x), int(event.y))
        iter = textview.get_iter_at_location(x, y)

        for tag in iter.get_tags():
            link = tag.get_data('link')
            if not link:
                continue
            self.emit('link-clicked', link)
            break
        return False

    def _on_buffer_mark_set(self, buffer, iter, mark):
        self._update_annotations()

    def _on_map_event(self, widget, event):
        self._update_annotation_area()
        self._update_annotations()

    def _on_expose_event(self, widget, event):
        text_window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
        if event.window != text_window:
            return

        # Create the cairo context.
        ctx = event.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        ctx.rectangle(event.area.x, event.area.y, event.area.width,
                      event.area.height)
        ctx.clip()

        self.draw(ctx, *event.window.get_size())

    def draw(self, ctx, w, h):
        if ctx is None:
            return
        if not self.show_annotations:
            return

        # Draw the dashes that connect annotations to their marker.
        ctx.set_line_width(1)
        ctx.set_dash((3, 2))
        right_margin = self.get_right_margin()
        for annotation in self.anno_layout.get_children():
            mark_x, mark_y = self._get_annotation_mark_position(annotation)
            anno_x, anno_y, anno_w, anno_h, d = annotation.window.get_geometry(
            )
            path = [(mark_x, mark_y - 5), (mark_x, mark_y),
                    (w - right_margin, mark_y), (w, anno_y + anno_h / 2)]

            stroke_color = annotation.get_border_color()
            ctx.set_source_rgba(*color.to_rgba(stroke_color))
            ctx.move_to(*path[0])
            for x, y in path[1:]:
                ctx.line_to(x, y)
            ctx.stroke()

    def _get_annotation_mark_offset(self, view1, view2):
        mark1 = view1.annotation.start_mark
        mark2 = view2.annotation.start_mark
        iter1 = self.get_buffer().get_iter_at_mark(mark1)
        iter2 = self.get_buffer().get_iter_at_mark(mark2)
        rect1 = self.get_iter_location(iter1)
        rect2 = self.get_iter_location(iter2)
        if rect1.y != rect2.y:
            return rect1.y - rect2.y
        return rect2.x - rect1.x

    def _get_annotation_mark_position(self, view):
        start_mark = view.annotation.start_mark
        iter = self.get_buffer().get_iter_at_mark(start_mark)
        rect = self.get_iter_location(iter)
        mark_x, mark_y = rect.x, rect.y + rect.height
        return self.buffer_to_window_coords(gtk.TEXT_WINDOW_TEXT, mark_x,
                                            mark_y)

    def _update_annotation(self, annotation):
        # Resize the annotation.
        item_width = self.anno_width - 2 * self.anno_padding
        annotation.set_size_request(item_width, -1)

        # Find the x, y of the annotation's mark.
        mark_x, mark_y = self._get_annotation_mark_position(annotation)
        self.anno_layout.pull(annotation, mark_y)

    def _update_annotation_area(self):
        # Update the width and color of the annotation area.
        self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, self.anno_width)
        bg_color = self.get_style().base[gtk.STATE_NORMAL]
        window = self.get_window(gtk.TEXT_WINDOW_RIGHT)
        if window:
            window.set_background(bg_color)

    def _update_annotations(self):
        if not self.show_annotations:
            return

        # Sort the annotations by line/char number and update them.
        iter = self.get_buffer().get_end_iter()
        rect = self.get_iter_location(iter)
        height = rect.y + rect.height
        self.anno_layout.sort(self._get_annotation_mark_offset)
        for annotation in self.anno_layout.get_children():
            self._update_annotation(annotation)
        self.anno_layout.update(self.anno_width, height)

        # Update lines.
        self.queue_draw()

    def set_annotation_area_width(self, width, padding=10):
        self.anno_width = width
        self.anno_padding = padding
        self._update_annotations()

    def _on_annotation_added(self, buffer, annotation):
        if self.show_annotations == False:
            return
        annotation.set_display_buffer(self.buffer)
        view = AnnotationView(annotation)
        self.anno_views[annotation] = view
        for event in ('focus-in-event', 'focus-out-event'):
            view.connect(event, self._on_annotation_event, annotation, event)
        view.show_all()
        self._update_annotation_area()
        self.anno_layout.add(view)
        self.add_child_in_window(view, gtk.TEXT_WINDOW_RIGHT,
                                 self.anno_padding, 0)
        self._update_annotations()

    def _on_annotation_removed(self, buffer, annotation):
        view = self.anno_views[annotation]
        self.anno_layout.remove(view)
        self.remove(view)

    def _on_annotation_event(self, buffer, *args):
        annotation = args[-2]
        event_name = args[-1]
        self.emit('annotation-' + event_name, annotation)

    def set_show_annotations(self, active=True):
        if self.show_annotations == active:
            return

        # Unfortunately gtk.TextView deletes all children from the
        # border window if its size is 0. So we must re-add them when the
        # window reappears.
        self.show_annotations = active
        if active:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_added(self.buffer, annotation)
        else:
            for annotation in self.buffer.get_annotations():
                self._on_annotation_removed(self.buffer, annotation)
            self.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, 0)

    def set_handle_links(self, handle):
        """
        Defines whether the cursor is changed and whether clicks are accepted
        when hovering over text with tags that have data named "link"
        attached.
        """
        self.handle_links = handle