1 # Copyright (C) 2007 by Aiwota Programmer
2 # aiwotaprog@tetteke.tk
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22 from FukuiNoNamariExt import thread_view_extend
25 def get_approximate_char_height(pango_context):
26 desc = pango_context.get_font_description()
27 font = pango_context.load_font(desc)
28 ink, log = font.get_glyph_extents(0)
29 return log[3] / pango.SCALE + 2
36 def __init__(self, start_index, end_index, rectangle):
37 self.start_index = start_index
38 self.end_index = end_index
39 self.rectangle = rectangle
41 def is_on_xy(self, x, y):
42 left = self.rectangle.x
43 right = left + self.rectangle.width
44 top = self.rectangle.y
45 bottom = top + self.rectangle.height
46 return x >= left and x < right and y >= top and y < bottom
51 def __init__(self, pango_layout):
52 self.pango_layout = pango_layout
55 def recalc_char_widths(self):
61 def is_on_xy(self, x, y):
62 for line in self.line_list:
63 if line.is_on_xy(x, y):
67 def xy_to_index(self, x, y):
70 def build_line_list(self, x, y, width, left_margin):
73 line = Line(0, 0, gtk.gdk.Rectangle(
75 get_approximate_char_height(self.pango_layout.get_context())))
76 self.line_list.append(line)
80 def get_text(self, selection=False, start_index=0, end_index=0xffffff):
83 def draw(self, drawingarea, y_offset, pango_layout,
84 selection=False, start_index=0, end_index=0xffffff):
90 def __init__(self, text, pango_layout):
92 self.pango_layout = pango_layout
94 attrlist = self._get_attrs()
95 self.widths = thread_view_extend.get_char_width(
96 pango_layout.get_context(), text, attrlist)
100 def recalc_char_widths(self):
101 attrlist = self._get_attrs()
102 self.widths = thread_view_extend.get_char_width(
103 self.pango_layout.get_context(), self.text, attrlist)
105 def _get_attrs(self):
106 attrs = pango.AttrList()
109 def is_on_xy(self, x, y):
110 for line in self.line_list:
111 if line.is_on_xy(x, y):
115 def xy_to_index(self, x, y):
116 for line in self.line_list:
117 top = line.rectangle.y
118 bottom = top + line.rectangle.height
119 if y >= top and y < bottom:
120 sum_of_widths = line.rectangle.x
121 index = line.start_index
122 for width in self.widths[line.start_index:line.end_index]:
123 if sum_of_widths + width/2 > x:
125 sum_of_widths += width
129 def build_line_list(self, x, y, width, left_margin):
132 current_line_start_index = 0
135 current_line_width = 0
137 ch_h = get_approximate_char_height(self.pango_layout.get_context())
139 for index, ch in enumerate(self.text):
140 ch_w = self.widths[index]
141 if current_line_x + current_line_width + ch_w > width:
143 current_line_start_index, index,
145 current_line_x, current_line_y,
146 current_line_width, ch_h))
147 self.line_list.append(line)
149 current_line_start_index = index
150 current_line_x = left_margin
151 current_line_y += ch_h
152 current_line_width = ch_w
154 current_line_width += ch_w
156 if current_line_start_index < len(self.text):
157 line = Line(current_line_start_index, len(self.text),
158 gtk.gdk.Rectangle(current_line_x,
162 self.line_list.append(line)
164 current_line_x += current_line_width
166 return current_line_x, current_line_y
168 def get_text(self, selection=False, start_index=0, end_index=0xffffff):
172 for line in self.line_list:
174 t = self.text[line.start_index:line.end_index]
176 s = start_index - line.start_index
178 s = min(s, line.end_index - line.start_index)
180 e = end_index - line.start_index
181 e = min(e, line.end_index - line.start_index)
190 def draw(self, drawingarea, y_offset, pango_layout,
191 selection=False, start_index=0, end_index=0xffffff):
193 selection_fg = drawingarea.style.fg[3]
194 selection_bg = drawingarea.style.bg[3]
196 for line in self.line_list:
198 text = self.text[line.start_index:line.end_index]
199 u_text = text.encode("utf8")
200 gc = drawingarea.window.new_gc()
201 attrs = self._get_attrs()
204 s = start_index - line.start_index
206 s = min(s, line.end_index - line.start_index)
207 s = len(text[:s].encode("utf8"))
209 e = end_index - line.start_index
210 e = min(e, line.end_index - line.start_index)
212 e = len(text[:e].encode("utf8"))
214 selection_all_attr_fg = pango.AttrForeground(
215 selection_fg.red, selection_fg.green, selection_fg.blue,
217 selection_all_attr_bg= pango.AttrBackground(
218 selection_bg.red, selection_bg.green, selection_bg.blue,
220 attrs.insert(selection_all_attr_fg)
221 attrs.insert(selection_all_attr_bg)
223 pango_layout.set_text(u_text)
224 pango_layout.set_attributes(attrs)
225 drawingarea.window.draw_layout(
226 gc, line.rectangle.x, line.rectangle.y + y_offset,
230 class ElementBoldText(ElementText):
232 def _get_attrs(self):
233 attrlist = pango.AttrList()
234 attr = pango.AttrWeight(pango.WEIGHT_BOLD,
236 attrlist.insert(attr)
240 class ElementLink(ElementText):
242 def __init__(self, text, href, pango_layout):
244 ElementText.__init__(self, text, pango_layout)
246 def _get_attrs(self):
247 attrlist = pango.AttrList()
248 attr = pango.AttrUnderline(pango.UNDERLINE_SINGLE,
250 attrlist.insert(attr)
257 def __init__(self, left_margin, resnum, pango_layout):
258 self.element_list = [ElementEmpty(pango_layout)]
261 self.pango_layout = pango_layout
262 self.left_margin = left_margin
267 def add_text(self, text, bold, href):
268 if isinstance(self.element_list[0], ElementEmpty):
269 self.element_list = []
272 element = ElementLink(text, href, self.pango_layout)
273 self.element_list.append(element)
275 element = ElementBoldText(text, self.pango_layout)
276 self.element_list.append(element)
278 element = ElementText(text, self.pango_layout)
279 self.element_list.append(element)
281 def get_element_from_xy(self, x, y):
282 for element in self.element_list:
283 if element.is_on_xy(x, y):
287 def get_close_element_from_xy(self, x, y):
288 x= max(x, self.left_margin)
289 element = self.get_element_from_xy(x, y)
290 if element is None and len(self.element_list) != 0:
291 element = self.element_list[len(self.element_list) - 1]
294 def recalc_char_widths(self):
295 for element in self.element_list:
296 element.recalc_char_widths()
298 def set_width(self, width):
302 current_x = self.left_margin
305 for element in self.element_list:
306 current_x, current_y = element.build_line_list(
307 current_x, current_y, width, self.left_margin)
309 self.height = current_y + get_approximate_char_height(self.pango_layout.get_context())
311 def get_pixel_size(self):
312 return self.width, self.height
314 def get_text(self, selection_start, selection_end):
315 s_s = selection_start
317 s_l, s_e, s_i = selection_start
318 e_l, e_e, e_i = selection_end
322 if (s_l is None or s_e is None or s_i is None or
323 e_l is None or e_e is None or e_i is None or
324 self.posY < s_l.posY or self.posY > e_l.posY):
329 elif self.posY > s_s[0].posY and self.posY < e_s[0].posY:
331 for element in self.element_list:
332 text += element.get_text(selection=True)
334 elif self == s_s[0] and self == e_s[0]:
338 for element in self.element_list:
346 text += element.get_text(selection=True, start_index=start,
351 text += element.get_text(
352 selection=True, end_index=end)
354 text += element.get_text(selection=True)
360 for element in self.element_list:
364 text += element.get_text(selection=True, start_index=start)
366 text += element.get_text(selection=True)
372 for element in self.element_list:
375 text += element.get_text(selection=True, end_index=e_i)
378 text += element.get_text(selection=True)
387 def draw(self, drawingarea, x_offset, y_offset,
388 start_selection, end_selection):
390 s_s = start_selection
396 if (s_l is None or s_e is None or s_i is None or
397 e_l is None or e_e is None or e_i is None or
398 self.posY < s_l.posY or self.posY > e_l.posY):
400 for element in self.element_list:
401 element.draw(drawingarea, y_offset, self.pango_layout)
403 elif self.posY > s_s[0].posY and self.posY < e_s[0].posY:
405 for element in self.element_list:
406 element.draw(drawingarea, y_offset, self.pango_layout,
409 elif self == s_s[0] and self == e_s[0]:
413 for element in self.element_list:
421 element.draw(drawingarea, y_offset, self.pango_layout,
428 element.draw(drawingarea, y_offset, self.pango_layout,
429 selection=True, end_index=end)
431 element.draw(drawingarea, y_offset, self.pango_layout,
438 for element in self.element_list:
442 element.draw(drawingarea, y_offset, self.pango_layout,
443 selection=selection, start_index = start)
445 element.draw(drawingarea, y_offset, self.pango_layout,
452 for element in self.element_list:
455 element.draw(drawingarea, y_offset, self.pango_layout,
456 selection=selection, end_index=e_i)
459 element.draw(drawingarea, y_offset, self.pango_layout,
463 for element in self.element_list:
464 element.draw(drawingarea, y_offset, self.pango_layout)
468 class ThreadView(gtk.HBox):
469 hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
470 regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
471 arrow_cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
474 gtk.HBox.__init__(self, False, 0)
475 self.drawingarea = gtk.DrawingArea()
476 self.vscrollbar = gtk.VScrollbar()
477 self.pack_start(self.drawingarea)
478 self.pack_start(self.vscrollbar, expand=False)
479 self.adjustment = self.vscrollbar.get_adjustment()
481 self.drawingarea.add_events(
482 gtk.gdk.SCROLL_MASK |
483 gtk.gdk.POINTER_MOTION_MASK |
484 gtk.gdk.BUTTON_PRESS_MASK |
485 gtk.gdk.BUTTON_RELEASE_MASK)
487 self.drawingarea_prev_width = 0
489 self.drawingarea.connect(
490 "expose-event", self.on_drawingarea_expose_event)
491 self.drawingarea.connect(
492 "configure-event", self.on_drawingarea_configure_event)
493 self.drawingarea.connect(
494 "scroll-event", self.on_drawingarea_scroll_event)
495 self.drawingarea.connect(
496 "motion-notify-event", self.on_drawingrarea_motion_notify_event)
497 self.drawingarea.connect(
498 "button-press-event", self.on_drawingarea_button_press_event)
499 self.drawingarea.connect(
500 "button-release-event", self.on_drawingarea_button_release_event)
501 self.drawingarea.connect(
502 "style-set", self.on_drawingarea_style_set)
503 self.vscrollbar.connect(
504 "value-changed", self.on_vscrollbar_value_changed)
506 self.pango_layout = self.drawingarea.create_pango_layout("")
508 self.initialize_buffer()
510 self.on_uri_clicked = self._on_uri_clicked
512 self.button1_pressed = False
513 self.current_pressed_uri = None
515 self.popupmenu = None
516 self.menu_openuri = None
517 self.menu_copylinkaddress = None
518 self.menu_separator_link = None
519 self.menu_copyselection = None
520 self.menu_openasuri = None
521 self.menu_separator_selection = None
523 self.menud_uri = None
526 self.button_pressed_pt = (None, None, None)
527 self.button_moving_pt = (None, None, None)
529 def _on_uri_clicked(self, uri):
530 print uri, "clicked!!!!"
532 def initialize_buffer(self):
533 self.res_layout_list = []
534 self.layout_posY_map = [(0, [])]
536 def add_layout(self, res_layout):
537 if (len(self.res_layout_list) != 0):
538 last = self.res_layout_list[len(self.res_layout_list)-1]
539 x, y = last.get_pixel_size()
540 res_layout.posY = last.posY + y
541 self.set_layout_width(res_layout)
542 res_layout.list_index = len(self.res_layout_list)
543 self.res_layout_list.append(res_layout)
545 if len(self.layout_posY_map[len(self.layout_posY_map)-1][1]) == 128:
546 self.layout_posY_map.append((res_layout.posY, []))
547 self.layout_posY_map[len(self.layout_posY_map)-1][1].append(res_layout)
549 x, y = res_layout.get_pixel_size()
550 self.adjustment.upper = res_layout.posY + y
552 self.change_vscrollbar_visible()
554 def create_res_layout(self, left_margin, resnum):
555 return ResLayout(left_margin, resnum, self.pango_layout)
557 def set_layout_width(self, layout):
558 width = self.drawingarea.allocation.width
559 layout.set_width(width)
562 self.drawingarea.queue_draw()
564 def wrap_relayout(self):
565 # before relayout, find top layout on gdkwindow
566 top_layout = self.get_layout_on_y(self.adjustment.value)
569 if top_layout is not None:
570 delta = top_layout.posY - self.vscrollbar.get_value()
573 self.drawingarea_prev_width = self.drawingarea.allocation.width
575 # after relayout, set vscrollbar.value to top layout's posY
576 if top_layout is not None:
577 self.vscrollbar.set_value(top_layout.posY - delta)
580 self.layout_posY_map = [(0, [])]
582 width = self.drawingarea.allocation.width
584 for layout in self.res_layout_list:
585 layout.set_width(width)
586 layout.posY = sum_height
587 x, y = layout.get_pixel_size()
590 if len(self.layout_posY_map[len(self.layout_posY_map)-1][1])==128:
591 self.layout_posY_map.append((layout.posY, []))
592 self.layout_posY_map[len(self.layout_posY_map)-1][1].append(layout)
594 self.vscrollbar.set_range(0, sum_height)
595 self.change_vscrollbar_visible()
597 def change_vscrollbar_visible(self):
598 if self.adjustment.upper < self.adjustment.page_size:
599 self.vscrollbar.hide()
601 self.vscrollbar.show()
603 def jump(self, value):
604 self.vscrollbar.set_value(value)
606 def jump_to_layout(self, layout):
607 self.jump(layout.posY)
609 def jump_to_the_end(self):
610 value = self.adjustment.upper - self.adjustment.page_size
611 self.vscrollbar.set_value(value)
613 def jump_to_res(self, resnum):
614 for layout in self.res_layout_list:
615 if layout.resnum == resnum:
616 self.jump_to_layout(layout)
621 def _get_selection_start_end(self):
622 pressed_layout, pressed_element, pressed_index = self.button_pressed_pt
623 moving_layout, moving_element, moving_index = self.button_moving_pt
625 if (pressed_layout is None or pressed_element is None or
626 pressed_index is None or moving_layout is None or
627 moving_element is None or moving_index is None):
628 return (None, None, None), (None, None, None)
630 if pressed_layout == moving_layout:
631 if pressed_element == moving_element:
632 if moving_index < pressed_index:
633 return self.button_moving_pt, self.button_pressed_pt
635 pressed_element_index = pressed_layout.element_list.index(
637 moving_element_index = moving_layout.element_list.index(
639 if moving_element_index < pressed_element_index:
640 return self.button_moving_pt, self.button_pressed_pt
641 elif moving_layout.posY < pressed_layout.posY:
642 return self.button_moving_pt, self.button_pressed_pt
644 return self.button_pressed_pt, self.button_moving_pt
646 def draw_viewport(self, area):
647 view_y = self.vscrollbar.get_value()
648 self.drawingarea.window.draw_rectangle(
649 self.drawingarea.style.base_gc[0],
650 True, area.x, area.y, area.width, area.height)
652 selection_start, selection_end = self._get_selection_start_end()
655 top_layout = self.get_layout_on_y(view_y)
658 index = top_layout.list_index
659 while index < len(self.res_layout_list):
660 layout = self.res_layout_list[index]
661 w, h = layout.get_pixel_size()
662 layout_top = layout.posY
663 layout_bottom = layout.posY + h
664 area_top = view_y + area.y
665 area_bottom = view_y + area.y + area.height
666 if layout_top <= area_bottom and layout_bottom >= area_top:
667 layout.draw(self.drawingarea,
668 0, layout.posY - int(view_y),
669 selection_start, selection_end)
670 if layout_top > area_bottom:
675 def transform_coordinate_gdk_to_adj(self, y):
676 return y + self.vscrollbar.get_value()
678 def transform_coordinate_adj_to_layout(self, x, y, layout):
679 return x, y - layout.posY
681 def transform_coordinate_gdk_to_layout(self, x, y, layout):
682 return self.transform_coordinate_adj_to_layout(
683 x, self.transform_coordinate_gdk_to_adj(y), layout)
685 def get_layout_on_y(self, y):
687 for pos, lay_lst in self.layout_posY_map:
690 layout_list = lay_lst
693 for lay in layout_list:
700 def ptrpos_to_layout(self, x, y):
701 # transform coordinate, GdkWindow -> adjustment
702 adj_y = self.transform_coordinate_gdk_to_adj(y)
703 return self.get_layout_on_y(adj_y)
705 def ptrpos_to_uri(self, x, y):
706 # x, y is GdkWindow coordinate
708 layout = self.ptrpos_to_layout(x, y)
711 return None, None, None
713 # transform coordinate, GdkWindow -> res_layout_list
714 lay_x, lay_y = self.transform_coordinate_gdk_to_layout(x, y, layout)
717 element = layout.get_element_from_xy(lay_x, lay_y)
718 if isinstance(element, ElementLink):
719 return element.href, layout, element
721 return None, layout, None
723 def get_selected_text(self):
724 selection_start, selection_end = self._get_selection_start_end()
725 s_l, s_e, s_i = selection_start
726 e_l, e_e, e_i = selection_end
728 if (s_l is None or s_e is None or s_i is None or
729 e_l is None or e_e is None or e_i is None):
733 index = s_l.list_index
737 layout = self.res_layout_list[index]
739 text += layout.get_text(selection_start, selection_end)
747 def _set_button_pressed_pt(self, pt):
748 self.button_pressed_pt = (None, None, None)
753 layout = self.ptrpos_to_layout(x, y)
757 x, y = self.transform_coordinate_gdk_to_layout(x, y, layout)
758 element = layout.get_element_from_xy(x, y)
760 element = layout.get_close_element_from_xy(x, y)
765 index = element.xy_to_index(x, y)
769 self.button_pressed_pt = (layout, element, index)
771 def _set_button_moving_pt(self, pt):
772 self.button_moving_pt = (None, None, None)
777 layout = self.ptrpos_to_layout(x, y)
781 x, y = self.transform_coordinate_gdk_to_layout(x, y, layout)
782 element = layout.get_element_from_xy(x, y)
784 element = layout.get_close_element_from_xy(x, y)
789 index = element.xy_to_index(x, y)
793 self.button_moving_pt = (layout, element, index)
795 def on_drawingarea_expose_event(self, widget, event, data=None):
796 self.draw_viewport(event.area)
798 def on_drawingarea_configure_event(self, widget, event, data=None):
799 if event.width != self.drawingarea_prev_width:
802 self.adjustment.page_size = self.drawingarea.allocation.height
803 self.vscrollbar.set_increments(20, self.drawingarea.allocation.height)
805 # re-set 'value' for prevent overflow
806 self.vscrollbar.set_value(self.vscrollbar.get_value())
807 self.change_vscrollbar_visible()
809 def on_vscrollbar_value_changed(self, widget, data=None):
810 self.drawingarea.queue_draw()
812 def on_drawingarea_scroll_event(self, widget, event, data=None):
813 self.vscrollbar.emit("scroll-event", event)
815 def on_drawingrarea_motion_notify_event(self, widget, event, data=None):
816 if event.state & gtk.gdk.BUTTON1_MASK != gtk.gdk.BUTTON1_MASK:
817 self.button1_pressed = False
819 if self.button1_pressed and self.current_pressed_uri is None:
820 old_lay, old_elem, old_idx = self.button_moving_pt
821 self._set_button_moving_pt((event.x, event.y))
822 new_lay, new_elem, new_idx = self.button_moving_pt
823 if (old_lay != new_lay
824 or old_elem != new_elem
825 or old_idx != new_idx):
826 view_y = self.vscrollbar.get_value()
829 o_width, o_height = old_lay.get_pixel_size()
830 n_width, n_height = new_lay.get_pixel_size()
833 height = max(o_y, n_y) - y
834 if o_y > n_y: height += o_height
835 else: height += n_height
839 self.drawingarea.queue_draw_area(0, y, n_width, height+1)
840 #self.drawingarea.window.process_updates(False)
842 cursor = ThreadView.regular_cursor
844 uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
846 cursor = ThreadView.arrow_cursor
848 if uri is not None and uri != "":
849 cursor = ThreadView.hand_cursor
851 self.drawingarea.window.set_cursor(cursor)
853 def on_drawingarea_button_press_event(self, widget, event, data=None):
854 if event.button == 1:
855 self.current_pressed_uri = None
856 self.button1_pressed = True
857 uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
858 if uri is not None and layout is not None and element is not None:
859 self.current_pressed_uri = (uri, layout, element)
861 self._set_button_moving_pt((event.x, event.y))
862 self._set_button_pressed_pt((event.x, event.y))
863 self.drawingarea.queue_draw()
865 elif event.button == 3:
867 uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
868 if uri is not None and layout is not None and element is not None:
869 self.menu_openuri.show()
870 self.menu_copylinkaddress.show()
871 self.menu_separator_link.show()
872 self.menu_openuri.uri = uri
873 self.menu_copylinkaddress.uri = uri
875 self.menu_openuri.hide()
876 self.menu_copylinkaddress.hide()
877 self.menu_separator_link.hide()
878 self.menu_openuri.uri = None
879 self.menu_copylinkaddress.uri = None
881 text = self.get_selected_text()
882 if text and len(text) > 0:
883 self.menu_copyselection.show()
884 self.menu_openasuri.show()
885 self.menu_separator_selection.show()
887 self.menu_copyselection.hide()
888 self.menu_openasuri.hide()
889 self.menu_separator_selection.hide()
891 self.popupmenu.popup(None, None, None, event.button, time)
895 def on_drawingarea_button_release_event(self, widget, event, data=None):
896 if event.button == 1:
897 button1_pressed = self.button1_pressed
898 self.button1_pressed = False
900 if button1_pressed and self.current_pressed_uri is not None:
901 uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
902 p_uri, p_layout, p_element = self.current_pressed_uri
903 self.current_pressed_uri = None
904 if (uri == p_uri and layout == p_layout and
905 element == p_element):
906 self.on_uri_clicked(uri)
908 def on_drawingarea_style_set(self, widget, previous_style, data=None):
909 if previous_style is None:
912 new = widget.style.font_desc.hash()
913 old = previous_style.font_desc.hash()
915 for layout in self.res_layout_list:
916 layout.recalc_char_widths()