OSDN Git Service

Replace PangoLayout with ResLayout.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_view.py
index b256795..f3d2e4b 100644 (file)
@@ -19,9 +19,178 @@ import pygtk
 pygtk.require('2.0')
 import gtk
 import pango
+from FukuiNoNamariExt import thread_view_extend
+
+
+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 ElementText:
+
+    def __init__(self, text, pango_layout):
+        self.text = text
+
+        attrlist = self._get_attrs()
+        self.widths = thread_view_extend.get_char_width(
+            pango_layout.get_context(), text, attrlist)
+
+        self.line_list = []
+
+    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 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
+
+        for index, ch in enumerate(self.text):
+            ch_w = self.widths[index]
+            ch_h = Line.HEIGHT
+            if current_line_x + current_line_width + ch_w > width:
+                line = Line(
+                    current_line_start_index, index,
+                    gtk.gdk.Rectangle(
+                    current_line_x, current_line_y,
+                    current_line_width, Line.HEIGHT))
+                self.line_list.append(line)
+
+                current_line_start_index = index
+                current_line_x = left_margin
+                current_line_y += Line.HEIGHT
+                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),
+                        gtk.gdk.Rectangle(current_line_x,
+                                          current_line_y,
+                                          current_line_width,
+                                          Line.HEIGHT))
+            self.line_list.append(line)
+
+            current_line_x += current_line_width
+
+        return current_line_x, current_line_y
+
+    def draw(self, drawable, y_offset, pango_layout):
+
+        for line in self.line_list:
+            text = self.text[line.start_index:line.end_index]
+            gc = drawable.new_gc()
+            pango_layout.set_text(text)
+            attrs = self._get_attrs()
+            if attrs:
+                pango_layout.set_attributes(attrs)
+            drawable.draw_layout(gc,
+                                 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
+
+
+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 = []
+        self.width = 0
+        self.height = 0
+        self.pango_layout = pango_layout
+        self.left_margin = left_margin
+        self.resnum = resnum
+        self.posY = 0
+
+    def add_text(self, text, bold, href):
+        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 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 + Line.HEIGHT
+
+    def get_pixel_size(self):
+        return self.width, self.height
+
+    def draw(self, drawable, x_offset, y_offset):
+
+        for element in self.element_list:
+            element.draw(drawable, y_offset, self.pango_layout)
 
 
 class ThreadView(gtk.HBox):
+    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)
@@ -31,44 +200,72 @@ class ThreadView(gtk.HBox):
         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.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.vscrollbar.connect(
             "value-changed", self.on_vscrollbar_value_changed)
 
+        self.pango_layout = self.drawingarea.create_pango_layout("")
+
         self.initialize_buffer()
 
+        self.on_uri_clicked = self._on_uri_clicked
+
+        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
+
+    def _on_uri_clicked(self, uri):
+        print uri, "clicked!!!!"
+
     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
+            res_layout.posY = last.posY + y
+        self.set_layout_width(res_layout)
+        self.res_layout_list.append(res_layout)
+        x, y = res_layout.get_pixel_size()
+        self.adjustment.upper = res_layout.posY + y
         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()
@@ -76,34 +273,39 @@ class ThreadView(gtk.HBox):
     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.vscrollbar.set_range(0, sum_height)
+        self.change_vscrollbar_visible()
+
+    def change_vscrollbar_visible(self):
+        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()
+        view_y = self.vscrollbar.get_value()
         self.drawingarea.window.draw_rectangle(
             self.drawingarea.style.base_gc[0],
             True, 0, 0,
@@ -111,39 +313,147 @@ class ThreadView(gtk.HBox):
             self.drawingarea.allocation.height)
 
         gc = self.drawingarea.window.new_gc()
-        for layout in self.pangolayout:
+        for layout in self.res_layout_list:
             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)
+                layout.draw(self.drawingarea.window,
+                            0, layout.posY - int(view_y))
+
+    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 ptrpos_to_layout(self, x, y):
+        # transform coordinate, GdkWindow -> adjustment
+        adj_x = x
+        adj_y = self.transform_coordinate_gdk_to_adj(y)
+        for lay in self.res_layout_list:
+            width, height = lay.get_pixel_size()
+            if (adj_y >= lay.posY and adj_y < lay.posY + height and
+                adj_x >= lay.left_margin):
+                return lay
+        return None
+
+    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 on_drawingarea_expose_event(self, widget, event, data=None):
         self.draw_viewport()
 
-    def on_drawingarea_size_allocate(self, widget, allocation, data=None):
-        if allocation.width != self.drawingarea_prev_width:
+    def on_drawingarea_configure_event(self, widget, event, data=None):
+        if event.width != self.drawingarea_prev_width:
+
+            # before relayout, find top layout on gdkwindow
+            top_layout = None
+            delta = 0
+            for lay in self.res_layout_list:
+                if lay.posY > self.adjustment.value:
+                    break
+                top_layout = lay
+
+            if top_layout is not None:
+                delta = top_layout.posY - self.vscrollbar.get_value()
+
             self.relayout()
-            self.drawingarea_prev_width = allocation.width
+            self.drawingarea_prev_width = event.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)
+
         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)
 
-    def on_drawingarea_button_press_event(self, widget, event, data=None):
-        self.drawingarea.queue_draw()
+        # re-set 'value' for prevent overflow
+        self.vscrollbar.set_value(self.vscrollbar.get_value())
+        self.change_vscrollbar_visible()
 
     def on_vscrollbar_value_changed(self, widget, data=None):
         self.drawingarea.queue_draw()
 
     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.vscrollbar.emit("scroll-event", event)
+
+    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
+
+        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.drawingarea.window.set_cursor(cursor)
+
+    def on_drawingarea_button_press_event(self, widget, event, data=None):
+        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)
+        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
+
+            self.menu_copyselection.hide()
+            self.menu_openasuri.hide()
+            self.menu_separator_selection.hide()
+
+            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_preesed_uri = None
+                if (uri == p_uri and layout == p_layout and
+                    element == p_element):
+                    self.on_uri_clicked(uri)