OSDN Git Service

Selected text colors follow the focus in ThreadView.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_view.py
index 313b06c..d7b0b43 100644 (file)
@@ -19,9 +19,26 @@ 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
@@ -41,9 +58,13 @@ class Line:
 
 class ElementEmpty:
 
-    def __init__(self):
+    def __init__(self, pango_layout):
+        self.pango_layout = pango_layout
         self.initialize()
 
+    def recalc_char_widths(self):
+        pass
+        
     def initialize(self):
         self.line_list = []
 
@@ -59,11 +80,16 @@ class ElementEmpty:
     def build_line_list(self, x, y, width, left_margin):
         self.initialize()
 
-        line = Line(0, 0, gtk.gdk.Rectangle(x, y, width - x, Line.HEIGHT))
+        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
@@ -71,15 +97,39 @@ 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):
+        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
@@ -112,47 +162,76 @@ class ElementText:
         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]
-            ch_h = Line.HEIGHT
             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, Line.HEIGHT))
+                    current_line_width, ch_h))
                 self.line_list.append(line)
 
                 current_line_start_index = index
                 current_line_x = left_margin
-                current_line_y += Line.HEIGHT
+                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),
-                        gtk.gdk.Rectangle(current_line_x,
+                        Rectangle(current_line_x,
                                           current_line_y,
                                           current_line_width,
-                                          Line.HEIGHT))
+                                          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):
 
-        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:
 
@@ -178,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)
 
 
@@ -191,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):
     
@@ -210,7 +294,7 @@ class ResLayout:
 # represent one line
 
     def __init__(self, left_margin, resnum, pango_layout):
-        self.element_list = [ElementEmpty()]
+        self.element_list = [ElementEmpty(pango_layout)]
         self.width = 0
         self.height = 0
         self.pango_layout = pango_layout
@@ -246,6 +330,10 @@ class ResLayout:
             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
@@ -257,11 +345,84 @@ class ResLayout:
             current_x, current_y = element.build_line_list(
                 current_x, current_y, width, self.left_margin)
 
-        self.height = current_y + Line.HEIGHT
+        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):
 
@@ -340,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)
@@ -351,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 |
@@ -376,6 +554,10 @@ class ThreadView(gtk.HBox):
             "button-press-event", self.on_drawingarea_button_press_event)
         self.drawingarea.connect(
             "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)
 
@@ -383,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
             
@@ -402,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):
@@ -418,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):
@@ -437,9 +610,22 @@ class ThreadView(gtk.HBox):
     def redraw(self):
         self.drawingarea.queue_draw()
 
-    def relayout(self):
-        self.layout_posY_map = [(0, [])]
+    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.res_layout_list:
@@ -448,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()
@@ -504,36 +686,26 @@ class ThreadView(gtk.HBox):
 
         return self.button_pressed_pt, self.button_moving_pt
 
-    def draw_viewport(self):
+    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)
+            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)
-        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_bottom = view_y + self.drawingarea.allocation.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()
@@ -546,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):
@@ -583,6 +773,29 @@ class ThreadView(gtk.HBox):
 
         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)
@@ -633,28 +846,11 @@ class ThreadView(gtk.HBox):
         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:
-
-            # 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 = 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.wrap_relayout()
 
         self.adjustment.page_size = self.drawingarea.allocation.height
         self.vscrollbar.set_increments(20, self.drawingarea.allocation.height)
@@ -673,14 +869,27 @@ class ThreadView(gtk.HBox):
         if event.state & gtk.gdk.BUTTON1_MASK != gtk.gdk.BUTTON1_MASK:
             self.button1_pressed = False
 
-        if self.button1_pressed:
+        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):
-                self.drawingarea.queue_draw()
+                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
@@ -691,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)
 
@@ -722,9 +932,15 @@ class ThreadView(gtk.HBox):
                 self.menu_openuri.uri = None
                 self.menu_copylinkaddress.uri = None
 
-            self.menu_copyselection.hide()
-            self.menu_openasuri.hide()
-            self.menu_separator_selection.hide()
+            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()
 
             self.popupmenu.popup(None, None, None, event.button, time)
             return True
@@ -738,7 +954,44 @@ class ThreadView(gtk.HBox):
             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
+                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:
+            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()