OSDN Git Service

Selected text colors follow the focus in ThreadView.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_view.py
index 6323435..d7b0b43 100644 (file)
@@ -19,6 +19,8 @@ import pygtk
 pygtk.require('2.0')
 import gtk
 import pango
+import gobject
+import itertools
 from FukuiNoNamariExt import thread_view_extend
 
 
@@ -29,6 +31,14 @@ def get_approximate_char_height(pango_context):
     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
@@ -70,7 +80,7 @@ class ElementEmpty:
     def build_line_list(self, x, y, width, left_margin):
         self.initialize()
 
-        line = Line(0, 0, gtk.gdk.Rectangle(
+        line = Line(0, 0, Rectangle(
             x, y, width - x,
             get_approximate_char_height(self.pango_layout.get_context())))
         self.line_list.append(line)
@@ -87,20 +97,38 @@ class ElementEmpty:
     
 class ElementText:
 
+    ch_width_dict = {}   # key: char, value: width
+
     def __init__(self, text, pango_layout):
         self.text = text
         self.pango_layout = pango_layout
 
-        attrlist = self._get_attrs()
-        self.widths = thread_view_extend.get_char_width(
-            pango_layout.get_context(), text, attrlist)
+        self.recalc_char_widths()
 
         self.line_list = []
 
     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)
+        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()
@@ -141,7 +169,7 @@ class ElementText:
             if current_line_x + current_line_width + ch_w > width:
                 line = Line(
                     current_line_start_index, index,
-                    gtk.gdk.Rectangle(
+                    Rectangle(
                     current_line_x, current_line_y,
                     current_line_width, ch_h))
                 self.line_list.append(line)
@@ -155,7 +183,7 @@ class ElementText:
 
         if current_line_start_index < len(self.text):
             line = Line(current_line_start_index, len(self.text),
-                        gtk.gdk.Rectangle(current_line_x,
+                        Rectangle(current_line_x,
                                           current_line_y,
                                           current_line_width,
                                           ch_h))
@@ -190,14 +218,20 @@ class ElementText:
     def draw(self, drawingarea, y_offset, pango_layout,
              selection=False, start_index=0, end_index=0xffffff):
 
-        selection_fg = drawingarea.style.fg[3]
-        selection_bg = drawingarea.style.bg[3]
+        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:
 
@@ -223,7 +257,7 @@ class ElementText:
             pango_layout.set_text(u_text)
             pango_layout.set_attributes(attrs)
             drawingarea.window.draw_layout(
-                gc, line.rectangle.x, line.rectangle.y + y_offset,
+                gc, int(line.rectangle.x), line.rectangle.y + y_offset,
                 pango_layout)
 
 
@@ -236,6 +270,11 @@ class ElementBoldText(ElementText):
         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):
     
@@ -462,10 +501,24 @@ class ResLayout:
         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)
@@ -473,12 +526,15 @@ class ThreadView(gtk.HBox):
     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.KEY_PRESS_MASK |
             gtk.gdk.SCROLL_MASK |
             gtk.gdk.POINTER_MOTION_MASK |
             gtk.gdk.BUTTON_PRESS_MASK |
@@ -500,6 +556,8 @@ class ThreadView(gtk.HBox):
             "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)
 
@@ -507,8 +565,6 @@ class ThreadView(gtk.HBox):
 
         self.initialize_buffer()
 
-        self.on_uri_clicked = self._on_uri_clicked
-
         self.button1_pressed = False
         self.current_pressed_uri = None
             
@@ -526,12 +582,8 @@ class ThreadView(gtk.HBox):
         self.button_pressed_pt = (None, None, None)
         self.button_moving_pt = (None, None, None)
 
-    def _on_uri_clicked(self, uri):
-        print uri, "clicked!!!!"
-
     def initialize_buffer(self):
         self.res_layout_list = []
-        self.layout_posY_map = [(0, [])]
 
     def add_layout(self, res_layout):
         if (len(self.res_layout_list) != 0):
@@ -542,13 +594,10 @@ class ThreadView(gtk.HBox):
         res_layout.list_index = len(self.res_layout_list)
         self.res_layout_list.append(res_layout)
 
-        if len(self.layout_posY_map[len(self.layout_posY_map)-1][1]) == 128:
-            self.layout_posY_map.append((res_layout.posY, []))
-        self.layout_posY_map[len(self.layout_posY_map)-1][1].append(res_layout)
-
         x, y = res_layout.get_pixel_size()
         self.adjustment.upper = res_layout.posY + y
-        self.redraw()
+        # do not use this method in a loop because expensive.
+        # self.redraw()
         self.change_vscrollbar_visible()
 
     def create_res_layout(self, left_margin, resnum):
@@ -577,8 +626,6 @@ class ThreadView(gtk.HBox):
             self.vscrollbar.set_value(top_layout.posY - delta)
 
     def relayout(self):
-        self.layout_posY_map = [(0, [])]
-
         width = self.drawingarea.allocation.width
         sum_height = 0
         for layout in self.res_layout_list:
@@ -587,15 +634,11 @@ class ThreadView(gtk.HBox):
             x, y = layout.get_pixel_size()
             sum_height += y
 
-            if len(self.layout_posY_map[len(self.layout_posY_map)-1][1])==128:
-                self.layout_posY_map.append((layout.posY, []))
-            self.layout_posY_map[len(self.layout_posY_map)-1][1].append(layout)
-
         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()
@@ -651,26 +694,18 @@ class ThreadView(gtk.HBox):
 
         selection_start, selection_end = self._get_selection_start_end()
 
-
         top_layout = self.get_layout_on_y(view_y)
-        index = 0
-        if top_layout:
-            index = top_layout.list_index
-        while index < len(self.res_layout_list):
-            layout = self.res_layout_list[index]
-            w, h = layout.get_pixel_size()
-            layout_top = layout.posY
-            layout_bottom = layout.posY + h
-            area_top = view_y + area.y
-            area_bottom = view_y + area.y + area.height
-            if layout_top <= area_bottom and layout_bottom >= area_top:
-                layout.draw(self.drawingarea,
-                            0, layout.posY - int(view_y),
-                            selection_start, selection_end)
-            if layout_top > area_bottom:
-                break
+        if top_layout is None:
+            return
+        #area_top = view_y + area.y
+        area_bottom = view_y + area.y + area.height
 
-            index += 1
+        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()
@@ -683,18 +718,36 @@ class ThreadView(gtk.HBox):
             x, self.transform_coordinate_gdk_to_adj(y), layout)
 
     def get_layout_on_y(self, y):
-        layout_list = None
-        for pos, lay_lst in self.layout_posY_map:
-            if pos > y:
-                break
-            layout_list = lay_lst
-        if layout_list:
-            layout = None
-            for lay in layout_list:
-                if lay.posY > y:
-                    break
-                layout = lay
-            return layout
+
+        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):
@@ -847,6 +900,7 @@ class ThreadView(gtk.HBox):
         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)
 
@@ -903,7 +957,7 @@ class ThreadView(gtk.HBox):
                 self.current_pressed_uri = None
                 if (uri == p_uri and layout == p_layout and
                     element == p_element):
-                    self.on_uri_clicked(uri)
+                    self.emit("uri-clicked-event", uri)
 
     def on_drawingarea_style_set(self, widget, previous_style, data=None):
         if previous_style is None:
@@ -915,3 +969,29 @@ class ThreadView(gtk.HBox):
             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()