OSDN Git Service

Selected text colors follow the focus in ThreadView.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_view.py
index 120bd64..d7b0b43 100644 (file)
@@ -19,154 +19,979 @@ import pygtk
 pygtk.require('2.0')
 import gtk
 import pango
+import gobject
+import itertools
+from FukuiNoNamariExt import thread_view_extend
+
+
+def get_approximate_char_height(pango_context):
+    desc = pango_context.get_font_description()
+    font = pango_context.load_font(desc)
+    ink, log = font.get_glyph_extents(0)
+    return log[3] / pango.SCALE + 2
+
+
+class Rectangle:
+    def __init__(self, x, y, width, height):
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
+
+
+class Line:
+
+    HEIGHT = 15
+
+    def __init__(self, start_index, end_index, rectangle):
+        self.start_index = start_index
+        self.end_index = end_index
+        self.rectangle = rectangle
+
+    def is_on_xy(self, x, y):
+        left = self.rectangle.x
+        right = left + self.rectangle.width
+        top = self.rectangle.y
+        bottom = top + self.rectangle.height
+        return x >= left and x < right and y >= top and y < bottom
+        
+
+class ElementEmpty:
+
+    def __init__(self, pango_layout):
+        self.pango_layout = pango_layout
+        self.initialize()
+
+    def recalc_char_widths(self):
+        pass
+        
+    def initialize(self):
+        self.line_list = []
+
+    def is_on_xy(self, x, y):
+        for line in self.line_list:
+            if line.is_on_xy(x, y):
+                return True
+        return False
+
+    def xy_to_index(self, x, y):
+        return 0
+
+    def build_line_list(self, x, y, width, left_margin):
+        self.initialize()
+
+        line = Line(0, 0, Rectangle(
+            x, y, width - x,
+            get_approximate_char_height(self.pango_layout.get_context())))
+        self.line_list.append(line)
+
+        return width, y
+
+    def get_text(self, selection=False, start_index=0, end_index=0xffffff):
+        return ""
+
+    def draw(self, drawingarea, y_offset, pango_layout,
+             selection=False, start_index=0, end_index=0xffffff):
+        pass
+
+    
+class ElementText:
+
+    ch_width_dict = {}   # key: char, value: width
+
+    def __init__(self, text, pango_layout):
+        self.text = text
+        self.pango_layout = pango_layout
+
+        self.recalc_char_widths()
+
+        self.line_list = []
+
+    def recalc_char_widths(self):
+        self.widths = [i for i in itertools.repeat(0, len(self.text))]
+
+        dict = self._get_ch_width_dict()
+        need_to_get = False
+        for index, ch in enumerate(self.text):
+            if ch not in dict:
+                need_to_get = True
+                break
+            else:
+                width = dict[ch]
+                self.widths[index] = width
+
+        if need_to_get:
+            attrlist = self._get_attrs()
+            self.widths = thread_view_extend.get_char_width(
+                self.pango_layout.get_context(), self.text, attrlist)
+            for index, width in enumerate(self.widths):
+                dict[self.text[index]] = self.widths[index]
+
+    def _get_ch_width_dict(self):
+        return ElementText.ch_width_dict
+
+    def _get_attrs(self):
+        attrs = pango.AttrList()
+        return attrs
+
+    def is_on_xy(self, x, y):
+        for line in self.line_list:
+            if line.is_on_xy(x, y):
+                return True
+        return False
+
+    def xy_to_index(self, x, y):
+        for line in self.line_list:
+            top = line.rectangle.y
+            bottom = top + line.rectangle.height
+            if y >= top and y < bottom:
+                sum_of_widths = line.rectangle.x
+                index = line.start_index
+                for width in self.widths[line.start_index:line.end_index]:
+                    if sum_of_widths + width/2 > x:
+                        break
+                    sum_of_widths += width
+                    index += 1
+                return index
+        
+    def build_line_list(self, x, y, width, left_margin):
+        self.line_list = []
+
+        current_line_start_index = 0
+        current_line_x = x
+        current_line_y = y
+        current_line_width = 0
+
+        ch_h = get_approximate_char_height(self.pango_layout.get_context())
+
+        for index, ch in enumerate(self.text):
+            ch_w = self.widths[index]
+            if current_line_x + current_line_width + ch_w > width:
+                line = Line(
+                    current_line_start_index, index,
+                    Rectangle(
+                    current_line_x, current_line_y,
+                    current_line_width, ch_h))
+                self.line_list.append(line)
+
+                current_line_start_index = index
+                current_line_x = left_margin
+                current_line_y += ch_h
+                current_line_width = ch_w
+            else:
+                current_line_width += ch_w
+
+        if current_line_start_index < len(self.text):
+            line = Line(current_line_start_index, len(self.text),
+                        Rectangle(current_line_x,
+                                          current_line_y,
+                                          current_line_width,
+                                          ch_h))
+            self.line_list.append(line)
+
+            current_line_x += current_line_width
+
+        return current_line_x, current_line_y
+
+    def get_text(self, selection=False, start_index=0, end_index=0xffffff):
+
+        text = ""
+
+        for line in self.line_list:
+
+            t = self.text[line.start_index:line.end_index]
+            if selection:
+                s = start_index - line.start_index
+                s = max(s, 0)
+                s = min(s, line.end_index - line.start_index)
+
+                e = end_index - line.start_index
+                e = min(e, line.end_index - line.start_index)
+                e = max(e, 0)
+
+                t = t[s:e]
+
+            text += t
+
+        return text
+
+    def draw(self, drawingarea, y_offset, pango_layout,
+             selection=False, start_index=0, end_index=0xffffff):
+
+        if drawingarea.get_property("has-focus"):
+            selection_fg = drawingarea.style.text[gtk.STATE_SELECTED]
+            selection_bg = drawingarea.style.base[gtk.STATE_SELECTED]
+        else:
+            selection_fg = drawingarea.style.text[gtk.STATE_ACTIVE]
+            selection_bg = drawingarea.style.base[gtk.STATE_ACTIVE]
+
+        for line in self.line_list:
+
+            text = self.text[line.start_index:line.end_index]
+            u_text = text.encode("utf8")
+            gc = drawingarea.window.new_gc()
+            gc.set_foreground(drawingarea.style.text[gtk.STATE_NORMAL])
+            gc.set_background(drawingarea.style.base[gtk.STATE_NORMAL])
+            attrs = self._get_attrs()
+            if selection:
+
+                s = start_index - line.start_index
+                s = max(s, 0)
+                s = min(s, line.end_index - line.start_index)
+                s = len(text[:s].encode("utf8"))
+
+                e = end_index - line.start_index
+                e = min(e, line.end_index - line.start_index)
+                e = max(e, 0)
+                e = len(text[:e].encode("utf8"))
+                    
+                selection_all_attr_fg = pango.AttrForeground(
+                    selection_fg.red, selection_fg.green, selection_fg.blue,
+                    s, e)
+                selection_all_attr_bg= pango.AttrBackground(
+                    selection_bg.red, selection_bg.green, selection_bg.blue,
+                    s, e)
+                attrs.insert(selection_all_attr_fg)
+                attrs.insert(selection_all_attr_bg)
+
+            pango_layout.set_text(u_text)
+            pango_layout.set_attributes(attrs)
+            drawingarea.window.draw_layout(
+                gc, int(line.rectangle.x), line.rectangle.y + y_offset,
+                pango_layout)
+
+
+class ElementBoldText(ElementText):
+
+    def _get_attrs(self):
+        attrlist = pango.AttrList()
+        attr = pango.AttrWeight(pango.WEIGHT_BOLD,
+                                   end_index=0xffffff)
+        attrlist.insert(attr)
+        return attrlist
+
+    def recalc_char_widths(self):
+        attrlist = self._get_attrs()
+        self.widths = thread_view_extend.get_char_width(
+            self.pango_layout.get_context(), self.text, attrlist)
+
+
+class ElementLink(ElementText):
+    
+    def __init__(self, text, href, pango_layout):
+        self.href = href
+        ElementText.__init__(self, text, pango_layout)
+
+    def _get_attrs(self):
+        attrlist = pango.AttrList()
+        attr = pango.AttrUnderline(pango.UNDERLINE_SINGLE,
+                                   end_index=0xffffff)
+        attrlist.insert(attr)
+        return attrlist
+
+
+class ResLayout:
+# represent one line
+
+    def __init__(self, left_margin, resnum, pango_layout):
+        self.element_list = [ElementEmpty(pango_layout)]
+        self.width = 0
+        self.height = 0
+        self.pango_layout = pango_layout
+        self.left_margin = left_margin
+        self.resnum = resnum
+        self.posY = 0
+        self.list_index = 0
+
+    def add_text(self, text, bold, href):
+        if isinstance(self.element_list[0], ElementEmpty):
+            self.element_list = []
+
+        if href:
+            element = ElementLink(text, href, self.pango_layout)
+            self.element_list.append(element)
+        elif bold:
+            element = ElementBoldText(text, self.pango_layout)
+            self.element_list.append(element)
+        else:
+            element = ElementText(text, self.pango_layout)
+            self.element_list.append(element)
+
+    def get_element_from_xy(self, x, y):
+        for element in self.element_list:
+            if element.is_on_xy(x, y):
+                return element
+        return None
+
+    def get_close_element_from_xy(self, x, y):
+        x= max(x, self.left_margin)
+        element = self.get_element_from_xy(x, y)
+        if element is None and len(self.element_list) != 0:
+            element = self.element_list[len(self.element_list) - 1]
+        return element
+
+    def recalc_char_widths(self):
+        for element in self.element_list:
+            element.recalc_char_widths()
+
+    def set_width(self, width):
+
+        self.width = width
+        
+        current_x = self.left_margin
+        current_y = 0
+
+        for element in self.element_list:
+            current_x, current_y = element.build_line_list(
+                current_x, current_y, width, self.left_margin)
+
+        self.height = current_y + get_approximate_char_height(self.pango_layout.get_context())
+
+    def get_pixel_size(self):
+        return self.width, self.height
+
+    def get_text(self, selection_start, selection_end):
+        s_s = selection_start
+        e_s = selection_end
+        s_l, s_e, s_i = selection_start
+        e_l, e_e, e_i = selection_end
+
+        text = ""
+
+        if (s_l is None or s_e is None or s_i is None or
+            e_l is None or e_e is None or e_i is None or
+            self.posY < s_l.posY or self.posY > e_l.posY):
+
+            # nothing to do
+            pass
+
+        elif self.posY > s_s[0].posY and self.posY < e_s[0].posY:
+
+            for element in self.element_list:
+                text += element.get_text(selection=True)
+
+        elif self == s_s[0] and self == e_s[0]:
+
+            selection = False
+
+            for element in self.element_list:
+                if s_e == element:
+                    selection = True
+                    start = s_i
+                    end = 0xffffff
+                    if e_e == element:
+                        end = e_i
+                        selection = False
+                    text += element.get_text(selection=True, start_index=start,
+                                             end_index=end)
+                elif e_e == element:
+                    end = e_i
+                    selection = False
+                    text += element.get_text(
+                        selection=True, end_index=end)
+                elif selection:
+                    text += element.get_text(selection=True)
+
+        elif self == s_s[0]:
+
+            selection = False
+
+            for element in self.element_list:
+                if s_e == element:
+                    selection = True
+                    start = s_i
+                    text += element.get_text(selection=True, start_index=start)
+                elif selection:
+                    text += element.get_text(selection=True)
+
+        elif self == e_s[0]:
+
+            selection = True
+
+            for element in self.element_list:
+                if e_e == element:
+                    end = e_i
+                    text += element.get_text(selection=True, end_index=e_i)
+                    selection = False
+                elif selection:
+                    text += element.get_text(selection=True)
+
+        else:
+            # nothing to do
+            pass
+
+        return text
+
+
+    def draw(self, drawingarea, x_offset, y_offset,
+             start_selection, end_selection):
+
+        s_s = start_selection
+        e_s = end_selection
+
+        s_l, s_e, s_i = s_s
+        e_l, e_e, e_i = e_s
+
+        if (s_l is None or s_e is None or s_i is None or
+            e_l is None or e_e is None or e_i is None or
+            self.posY < s_l.posY or self.posY > e_l.posY):
+
+            for element in self.element_list:
+                element.draw(drawingarea, y_offset, self.pango_layout)
+
+        elif self.posY > s_s[0].posY and self.posY < e_s[0].posY:
+
+            for element in self.element_list:
+                element.draw(drawingarea, y_offset, self.pango_layout,
+                             selection=True)
+
+        elif self == s_s[0] and self == e_s[0]:
+
+            selection = False
+
+            for element in self.element_list:
+                if s_e == element:
+                    selection = True
+                    start = s_i
+                    end = 0xffffff
+                    if e_e == element:
+                        end = e_i
+                        selection = False
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=True,
+                                 start_index=start,
+                                 end_index=end)
+                elif e_e == element:
+                    end = e_i
+                    selection = False
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=True, end_index=end)
+                else:
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=selection)
+
+        elif self == s_s[0]:
+
+            selection = False
+
+            for element in self.element_list:
+                if s_e == element:
+                    selection = True
+                    start = s_i
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=selection, start_index = start)
+                else:
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=selection)
+
+        elif self == e_s[0]:
+
+            selection = True
+
+            for element in self.element_list:
+                if e_e == element:
+                    end = e_i
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=selection, end_index=e_i)
+                    selection = False
+                else:
+                    element.draw(drawingarea, y_offset, self.pango_layout,
+                                 selection=selection)
+
+        else:
+            for element in self.element_list:
+                element.draw(drawingarea, y_offset, self.pango_layout)
+
+    def clone(self):
+        import copy
+        layout = ResLayout(self.left_margin, self.resnum, self.pango_layout)
+        layout.element_list = []
+        for element in self.element_list:
+            layout.element_list.append(copy.copy(element))
+        return layout
 
 
 class ThreadView(gtk.HBox):
+    __gsignals__ = {
+        "cursor-over-link-event":
+        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object, object, )),
+        "uri-clicked-event":
+        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (object, ))
+        }
+
+    hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
+    regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
+    arrow_cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
 
     def __init__(self):
         gtk.HBox.__init__(self, False, 0)
         self.drawingarea = gtk.DrawingArea()
+        self.drawingarea.set_property("can_focus", True)
+
         self.vscrollbar = gtk.VScrollbar()
         self.pack_start(self.drawingarea)
         self.pack_start(self.vscrollbar, expand=False)
         self.adjustment  = self.vscrollbar.get_adjustment()
 
-        self.drawingarea.add_events(gtk.gdk.SCROLL_MASK)
+        self.drawingarea.add_events(
+            gtk.gdk.KEY_PRESS_MASK |
+            gtk.gdk.SCROLL_MASK |
+            gtk.gdk.POINTER_MOTION_MASK |
+            gtk.gdk.BUTTON_PRESS_MASK |
+            gtk.gdk.BUTTON_RELEASE_MASK)
 
-        self.adjustment.step_increment = 20
         self.drawingarea_prev_width = 0
 
         self.drawingarea.connect(
             "expose-event", self.on_drawingarea_expose_event)
         self.drawingarea.connect(
-            "size-allocate", self.on_drawingarea_size_allocate)
+            "configure-event", self.on_drawingarea_configure_event)
+        self.drawingarea.connect(
+            "scroll-event", self.on_drawingarea_scroll_event)
+        self.drawingarea.connect(
+            "motion-notify-event", self.on_drawingrarea_motion_notify_event)
         self.drawingarea.connect(
             "button-press-event", self.on_drawingarea_button_press_event)
         self.drawingarea.connect(
-            "scroll-event", self.on_drawingarea_scroll_event)
+            "button-release-event", self.on_drawingarea_button_release_event)
+        self.drawingarea.connect(
+            "style-set", self.on_drawingarea_style_set)
+        self.drawingarea.connect(
+            "key-press-event", self.on_drawingarea_key_press_event)
         self.vscrollbar.connect(
             "value-changed", self.on_vscrollbar_value_changed)
 
+        self.pango_layout = self.drawingarea.create_pango_layout("")
+
         self.initialize_buffer()
 
+        self.button1_pressed = False
+        self.current_pressed_uri = None
+            
+        self.popupmenu = None
+        self.menu_openuri = None
+        self.menu_copylinkaddress = None
+        self.menu_separator_link = None
+        self.menu_copyselection = None
+        self.menu_openasuri = None
+        self.menu_separator_selection = None
+
+        self.menud_uri = None
+
+        # for selection
+        self.button_pressed_pt = (None, None, None)
+        self.button_moving_pt = (None, None, None)
+
     def initialize_buffer(self):
-        self.pangolayout = []
+        self.res_layout_list = []
 
-    def add_layout(self, pangolayout):
-        if (len(self.pangolayout) != 0):
-            last = self.pangolayout[len(self.pangolayout)-1]
+    def add_layout(self, res_layout):
+        if (len(self.res_layout_list) != 0):
+            last = self.res_layout_list[len(self.res_layout_list)-1]
             x, y = last.get_pixel_size()
-            pangolayout.posY = last.posY + y
-        self.set_layout_width(pangolayout)
-        self.pangolayout.append(pangolayout)
-        x, y = pangolayout.get_pixel_size()
-        self.adjustment.upper = pangolayout.posY + y
-        self.prevent_adjustment_overflow()
-        self.redraw()
+            res_layout.posY = last.posY + y
+        self.set_layout_width(res_layout)
+        res_layout.list_index = len(self.res_layout_list)
+        self.res_layout_list.append(res_layout)
+
+        x, y = res_layout.get_pixel_size()
+        self.adjustment.upper = res_layout.posY + y
+        # do not use this method in a loop because expensive.
+        # self.redraw()
         self.change_vscrollbar_visible()
 
-    def create_pango_layout(self, text):
-        return self.drawingarea.create_pango_layout(text)
+    def create_res_layout(self, left_margin, resnum):
+        return ResLayout(left_margin, resnum, self.pango_layout)
 
     def set_layout_width(self, layout):
         width = self.drawingarea.allocation.width
-        layout.set_width((width - layout.marginleft) * pango.SCALE)
+        layout.set_width(width)
         
     def redraw(self):
         self.drawingarea.queue_draw()
 
+    def wrap_relayout(self):
+        # before relayout, find top layout on gdkwindow
+        top_layout = self.get_layout_on_y(self.adjustment.value)
+        delta = 0
+
+        if top_layout is not None:
+            delta = top_layout.posY - self.vscrollbar.get_value()
+
+        self.relayout()
+        self.drawingarea_prev_width = self.drawingarea.allocation.width
+
+        # after relayout, set vscrollbar.value to top layout's posY
+        if top_layout is not None:
+            self.vscrollbar.set_value(top_layout.posY - delta)
+
     def relayout(self):
         width = self.drawingarea.allocation.width
         sum_height = 0
-        for layout in self.pangolayout:
-            layout.set_width((width - layout.marginleft) * pango.SCALE)
+        for layout in self.res_layout_list:
+            layout.set_width(width)
             layout.posY = sum_height
             x, y = layout.get_pixel_size()
             sum_height += y
-        self.adjustment.upper = sum_height
-        self.prevent_adjustment_overflow()
+
+        self.vscrollbar.set_range(0, sum_height)
         self.change_vscrollbar_visible()
 
     def change_vscrollbar_visible(self):
-        if self.adjustment.upper < self.adjustment.page_size:
+        if self.adjustment.upper <= self.adjustment.page_size:
             self.vscrollbar.hide()
         else:
             self.vscrollbar.show()
 
     def jump(self, value):
-        if value > self.adjustment.upper - self.adjustment.page_size:
-            value = self.adjustment.upper - self.adjustment.page_size
-        self.adjustment.set_value(value)
+        self.vscrollbar.set_value(value)
 
     def jump_to_layout(self, layout):
         self.jump(layout.posY)
         
     def jump_to_the_end(self):
         value = self.adjustment.upper - self.adjustment.page_size
-        self.adjustment.set_value(value)
+        self.vscrollbar.set_value(value)
 
     def jump_to_res(self, resnum):
-        for layout in self.pangolayout:
+        for layout in self.res_layout_list:
             if layout.resnum == resnum:
                 self.jump_to_layout(layout)
                 return True
         return False
 
-    def draw_viewport(self):
-        view_y = self.adjustment.get_value()
+
+    def _get_selection_start_end(self):
+        pressed_layout, pressed_element, pressed_index = self.button_pressed_pt
+        moving_layout, moving_element, moving_index = self.button_moving_pt
+
+        if (pressed_layout is None or pressed_element is None or
+            pressed_index is None or moving_layout is None or
+            moving_element is None or moving_index is None):
+            return (None, None, None), (None, None, None)
+        
+        if pressed_layout == moving_layout:
+            if pressed_element == moving_element:
+                if moving_index < pressed_index:
+                    return self.button_moving_pt, self.button_pressed_pt
+            else:
+                pressed_element_index = pressed_layout.element_list.index(
+                    pressed_element)
+                moving_element_index = moving_layout.element_list.index(
+                    moving_element)
+                if moving_element_index < pressed_element_index:
+                    return self.button_moving_pt, self.button_pressed_pt
+        elif moving_layout.posY < pressed_layout.posY:
+            return self.button_moving_pt, self.button_pressed_pt
+
+        return self.button_pressed_pt, self.button_moving_pt
+
+    def draw_viewport(self, area):
+        view_y = self.vscrollbar.get_value()
         self.drawingarea.window.draw_rectangle(
             self.drawingarea.style.base_gc[0],
-            True, 0, 0,
-            self.drawingarea.allocation.width,
-            self.drawingarea.allocation.height)
-
-        gc = self.drawingarea.window.new_gc()
-        for layout in self.pangolayout:
-            w, h = layout.get_pixel_size()
-            layout_top = layout.posY
-            layout_bottom = layout.posY + h
-            area_top = view_y
-            area_bottom = view_y + self.drawingarea.allocation.height
-            if layout_top <= area_bottom and layout_bottom >= area_top:
-                self.drawingarea.window.draw_layout(
-                    gc, layout.marginleft, layout.posY - int(view_y), layout)
+            True, area.x, area.y, area.width, area.height)
+
+        selection_start, selection_end = self._get_selection_start_end()
+
+        top_layout = self.get_layout_on_y(view_y)
+        if top_layout is None:
+            return
+        #area_top = view_y + area.y
+        area_bottom = view_y + area.y + area.height
+
+        iter = range(top_layout.list_index, len(self.res_layout_list))
+        iter = itertools.imap(lambda index: self.res_layout_list[index], iter)
+        iter = itertools.takewhile(lambda lay: lay.posY <= area_bottom, iter)
+        for layout in iter:
+            layout.draw(self.drawingarea, 0, layout.posY - int(view_y),
+                selection_start, selection_end)
+
+    def transform_coordinate_gdk_to_adj(self, y):
+        return y + self.vscrollbar.get_value()
+
+    def transform_coordinate_adj_to_layout(self, x, y, layout):
+        return x, y - layout.posY
+
+    def transform_coordinate_gdk_to_layout(self, x, y, layout):
+        return self.transform_coordinate_adj_to_layout(
+            x, self.transform_coordinate_gdk_to_adj(y), layout)
+
+    def get_layout_on_y(self, y):
+
+        def binary_search(lst, start, end, func):
+
+            if end - start <= 0:
+                return None
+
+            m = (start + end) / 2
+            ret = func(lst[m])
+
+            if ret == 0:
+                return m
+            if ret > 0:
+                return binary_search(lst, start, m, func)
+            return binary_search(lst, m+1, end, func)
+
+        def on_y(layout, _y):
+            top = layout.posY
+            width, height = layout.get_pixel_size()
+            bottom = top + height
+            if _y >= top and _y < bottom:
+                return 0
+            if _y < top:
+                return 1
+            return -1
+
+        ret = binary_search(
+            self.res_layout_list, 0, len(self.res_layout_list),
+            lambda x: on_y(x, y))
+        if ret is not None:
+            return self.res_layout_list[ret]
+        return None
+
+    def ptrpos_to_layout(self, x, y):
+        # transform coordinate, GdkWindow -> adjustment
+        adj_y = self.transform_coordinate_gdk_to_adj(y)
+        return self.get_layout_on_y(adj_y)
+
+    def ptrpos_to_uri(self,  x, y):
+        # x, y is GdkWindow coordinate
+
+        layout = self.ptrpos_to_layout(x, y)
+
+        if layout is None:
+            return None, None, None
+
+        # transform coordinate, GdkWindow -> res_layout_list
+        lay_x, lay_y = self.transform_coordinate_gdk_to_layout(x, y, layout)
+
+        # xy -> element
+        element = layout.get_element_from_xy(lay_x, lay_y)
+        if isinstance(element, ElementLink):
+            return element.href, layout, element
+
+        return None, layout, None
+
+    def get_selected_text(self):
+        selection_start, selection_end = self._get_selection_start_end()
+        s_l, s_e, s_i = selection_start
+        e_l, e_e, e_i = selection_end
+
+        if (s_l is None or s_e is None or s_i is None or
+            e_l is None or e_e is None or e_i is None):
+            return ""
+
+        text = ""
+        index = s_l.list_index
+        end = e_l.list_index
+
+        while index <= end:
+            layout = self.res_layout_list[index]
+
+            text += layout.get_text(selection_start, selection_end)
+            if index != end:
+                text += "\n"
+
+            index += 1
+
+        return text
+
+    def _set_button_pressed_pt(self, pt):
+        self.button_pressed_pt = (None, None, None)
+        if pt == None:
+            return
+
+        x, y = pt
+        layout = self.ptrpos_to_layout(x, y)
+        if layout is None:
+            return
+
+        x, y = self.transform_coordinate_gdk_to_layout(x, y, layout)
+        element = layout.get_element_from_xy(x, y)
+        if element is None:
+            element = layout.get_close_element_from_xy(x, y)
+
+        if element is None:
+            return
+
+        index = element.xy_to_index(x, y)
+        if index is None:
+            return
+
+        self.button_pressed_pt = (layout, element, index)
+
+    def _set_button_moving_pt(self, pt):
+        self.button_moving_pt = (None, None, None)
+        if pt == None:
+            return
+
+        x, y = pt
+        layout = self.ptrpos_to_layout(x, y)
+        if layout is None:
+            return
+
+        x, y = self.transform_coordinate_gdk_to_layout(x, y, layout)
+        element = layout.get_element_from_xy(x, y)
+        if element is None:
+            element = layout.get_close_element_from_xy(x, y)
+
+        if element is None:
+            return
+
+        index = element.xy_to_index(x, y)
+        if index is None:
+            return
+
+        self.button_moving_pt = (layout, element, index)
 
     def on_drawingarea_expose_event(self, widget, event, data=None):
-        self.draw_viewport()
+        self.draw_viewport(event.area)
+
+    def on_drawingarea_configure_event(self, widget, event, data=None):
+        if event.width != self.drawingarea_prev_width:
+            self.wrap_relayout()
 
-    def on_drawingarea_size_allocate(self, widget, allocation, data=None):
-        if allocation.width != self.drawingarea_prev_width:
-            self.relayout()
-            self.drawingarea_prev_width = allocation.width
         self.adjustment.page_size = self.drawingarea.allocation.height
-        self.adjustment.page_increment = self.drawingarea.allocation.height
+        self.vscrollbar.set_increments(20, self.drawingarea.allocation.height)
 
-        self.prevent_adjustment_overflow()
+        # re-set 'value' for prevent overflow
+        self.vscrollbar.set_value(self.vscrollbar.get_value())
         self.change_vscrollbar_visible()
 
-    def prevent_adjustment_overflow(self):
-        if (self.adjustment.value >
-            self.adjustment.upper - self.adjustment.page_size):
-            self.adjustment.value = (self.adjustment.upper -
-                                     self.adjustment.page_size)
+    def on_vscrollbar_value_changed(self, widget, data=None):
+        self.drawingarea.queue_draw()
+
+    def on_drawingarea_scroll_event(self, widget, event, data=None):
+        self.vscrollbar.emit("scroll-event", event)
 
-        if self.adjustment.value < 0:
-            self.adjustment.value = 0
+    def on_drawingrarea_motion_notify_event(self, widget, event, data=None):
+        if event.state & gtk.gdk.BUTTON1_MASK != gtk.gdk.BUTTON1_MASK:
+            self.button1_pressed = False
+
+        if self.button1_pressed and self.current_pressed_uri is None:
+            old_lay, old_elem, old_idx = self.button_moving_pt
+            self._set_button_moving_pt((event.x, event.y))
+            new_lay, new_elem, new_idx = self.button_moving_pt
+            if (old_lay != new_lay
+                or old_elem != new_elem
+                or old_idx != new_idx):
+                view_y = self.vscrollbar.get_value()
+                o_y = old_lay.posY
+                n_y = new_lay.posY
+                o_width, o_height = old_lay.get_pixel_size()
+                n_width, n_height = new_lay.get_pixel_size()
+
+                y = min(o_y, n_y)
+                height = max(o_y, n_y) - y
+                if o_y > n_y: height += o_height
+                else: height += n_height
+
+                y -= view_y
+
+                self.drawingarea.queue_draw_area(0, y, n_width, height+1)
+                #self.drawingarea.window.process_updates(False)
+
+        cursor = ThreadView.regular_cursor
+
+        uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
+        if layout is None:
+            cursor = ThreadView.arrow_cursor
+        else:
+            if uri is not None and uri != "":
+                cursor = ThreadView.hand_cursor
+                self.emit("cursor-over-link-event", event, uri)
+
+        self.drawingarea.window.set_cursor(cursor)
 
     def on_drawingarea_button_press_event(self, widget, event, data=None):
-        self.drawingarea.queue_draw()
+        if event.button == 1:
+            self.current_pressed_uri = None
+            self.button1_pressed = True
+            uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
+            if uri is not None and layout is not None and element is not None:
+                self.current_pressed_uri = (uri, layout, element)
+            else:
+                self._set_button_moving_pt((event.x, event.y))
+                self._set_button_pressed_pt((event.x, event.y))
+                self.drawingarea.queue_draw()
+                
+        elif event.button == 3:
+            time = event.time
+            uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
+            if uri is not None and layout is not None and element is not None:
+                self.menu_openuri.show()
+                self.menu_copylinkaddress.show()
+                self.menu_separator_link.show()
+                self.menu_openuri.uri = uri
+                self.menu_copylinkaddress.uri = uri
+            else:
+                self.menu_openuri.hide()
+                self.menu_copylinkaddress.hide()
+                self.menu_separator_link.hide()
+                self.menu_openuri.uri = None
+                self.menu_copylinkaddress.uri = None
 
-    def on_vscrollbar_value_changed(self, widget, data=None):
-        self.drawingarea.queue_draw()
+            text = self.get_selected_text()
+            if text and len(text) > 0:
+                self.menu_copyselection.show()
+                self.menu_openasuri.show()
+                self.menu_separator_selection.show()
+            else:
+                self.menu_copyselection.hide()
+                self.menu_openasuri.hide()
+                self.menu_separator_selection.hide()
 
-    def on_drawingarea_scroll_event(self, widget, event, data=None):
-        if event.direction == gtk.gdk.SCROLL_UP:
-            self.adjustment.value -= 66.476200804
-            if self.adjustment.value < self.adjustment.lower:
-                self.adjustment.value = self.adjustment.lower
-        if event.direction == gtk.gdk.SCROLL_DOWN:
-            self.adjustment.value += 66.476200804
-            max_value = self.adjustment.upper - self.adjustment.page_size
-            if self.adjustment.value > max_value:
-                self.adjustment.value = max_value
-        self.prevent_adjustment_overflow()
+            self.popupmenu.popup(None, None, None, event.button, time)
+            return True
+            
+
+    def on_drawingarea_button_release_event(self, widget, event, data=None):
+        if event.button == 1:
+            button1_pressed = self.button1_pressed
+            self.button1_pressed = False
+
+            if button1_pressed and self.current_pressed_uri is not None:
+                uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
+                p_uri, p_layout, p_element = self.current_pressed_uri
+                self.current_pressed_uri = None
+                if (uri == p_uri and layout == p_layout and
+                    element == p_element):
+                    self.emit("uri-clicked-event", uri)
+
+    def on_drawingarea_style_set(self, widget, previous_style, data=None):
+        if previous_style is None:
+            return False
+
+        new = widget.style.font_desc.hash()
+        old = previous_style.font_desc.hash()
+        if new != old:
+            for layout in self.res_layout_list:
+                layout.recalc_char_widths()
+            self.wrap_relayout()
+
+    def on_drawingarea_key_press_event(self, widget, event, data=None):
+        if event.type is not gtk.gdk.KEY_PRESS:
+            return
+
+        if event.keyval in (gtk.keysyms.Up, gtk.keysyms.Down,
+                            gtk.keysyms.Page_Up, gtk.keysyms.Page_Down,
+                            gtk.keysyms.Home):
+            value = self.vscrollbar.get_value()
+            if event.keyval == gtk.keysyms.Up:
+                step_increment = self.adjustment.get_property("step-increment")
+                value = value - step_increment
+            elif event.keyval == gtk.keysyms.Down:
+                step_increment = self.adjustment.get_property("step-increment")
+                value = value + step_increment
+            elif event.keyval == gtk.keysyms.Page_Up:
+                step_increment = self.adjustment.get_property("page-increment")
+                value = value - step_increment
+            elif event.keyval == gtk.keysyms.Page_Down:
+                step_increment = self.adjustment.get_property("page-increment")
+                value = value + step_increment
+            elif event.keyval == gtk.keysyms.Home:
+                value = 0
+            self.jump(value)
+        elif event.keyval == gtk.keysyms.End:
+            self.jump_to_the_end()