OSDN Git Service

Replace PangoLayout with ResLayout.
[fukui-no-namari/fukui-no-namari.git] / src / FukuiNoNamari / thread_view.py
1 # Copyright (C) 2007 by Aiwota Programmer
2 # aiwotaprog@tetteke.tk
3 #
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.
8 #
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.
13 #
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
17
18 import pygtk
19 pygtk.require('2.0')
20 import gtk
21 import pango
22 from FukuiNoNamariExt import thread_view_extend
23
24
25 class Line:
26
27     HEIGHT = 15
28
29     def __init__(self, start_index, end_index, rectangle):
30         self.start_index = start_index
31         self.end_index = end_index
32         self.rectangle = rectangle
33
34     def is_on_xy(self, x, y):
35         left = self.rectangle.x
36         right = left + self.rectangle.width
37         top = self.rectangle.y
38         bottom = top + self.rectangle.height
39         return x >= left and x < right and y >= top and y < bottom
40         
41
42 class ElementText:
43
44     def __init__(self, text, pango_layout):
45         self.text = text
46
47         attrlist = self._get_attrs()
48         self.widths = thread_view_extend.get_char_width(
49             pango_layout.get_context(), text, attrlist)
50
51         self.line_list = []
52
53     def _get_attrs(self):
54         attrs = pango.AttrList()
55         return attrs
56
57     def is_on_xy(self, x, y):
58         for line in self.line_list:
59             if line.is_on_xy(x, y):
60                 return True
61         return False
62
63     def build_line_list(self, x, y, width, left_margin):
64         self.line_list = []
65
66         current_line_start_index = 0
67         current_line_x = x
68         current_line_y = y
69         current_line_width = 0
70
71         for index, ch in enumerate(self.text):
72             ch_w = self.widths[index]
73             ch_h = Line.HEIGHT
74             if current_line_x + current_line_width + ch_w > width:
75                 line = Line(
76                     current_line_start_index, index,
77                     gtk.gdk.Rectangle(
78                     current_line_x, current_line_y,
79                     current_line_width, Line.HEIGHT))
80                 self.line_list.append(line)
81
82                 current_line_start_index = index
83                 current_line_x = left_margin
84                 current_line_y += Line.HEIGHT
85                 current_line_width = ch_w
86             else:
87                 current_line_width += ch_w
88
89         if current_line_start_index < len(self.text):
90             line = Line(current_line_start_index, len(self.text),
91                         gtk.gdk.Rectangle(current_line_x,
92                                           current_line_y,
93                                           current_line_width,
94                                           Line.HEIGHT))
95             self.line_list.append(line)
96
97             current_line_x += current_line_width
98
99         return current_line_x, current_line_y
100
101     def draw(self, drawable, y_offset, pango_layout):
102
103         for line in self.line_list:
104             text = self.text[line.start_index:line.end_index]
105             gc = drawable.new_gc()
106             pango_layout.set_text(text)
107             attrs = self._get_attrs()
108             if attrs:
109                 pango_layout.set_attributes(attrs)
110             drawable.draw_layout(gc,
111                                  line.rectangle.x, line.rectangle.y + y_offset,
112                                  pango_layout)
113
114
115 class ElementBoldText(ElementText):
116
117     def _get_attrs(self):
118         attrlist = pango.AttrList()
119         attr = pango.AttrWeight(pango.WEIGHT_BOLD,
120                                    end_index=0xffffff)
121         attrlist.insert(attr)
122         return attrlist
123
124
125 class ElementLink(ElementText):
126     
127     def __init__(self, text, href, pango_layout):
128         self.href = href
129         ElementText.__init__(self, text, pango_layout)
130
131     def _get_attrs(self):
132         attrlist = pango.AttrList()
133         attr = pango.AttrUnderline(pango.UNDERLINE_SINGLE,
134                                    end_index=0xffffff)
135         attrlist.insert(attr)
136         return attrlist
137
138
139 class ResLayout:
140 # represent one line
141
142     def __init__(self, left_margin, resnum, pango_layout):
143         self.element_list = []
144         self.width = 0
145         self.height = 0
146         self.pango_layout = pango_layout
147         self.left_margin = left_margin
148         self.resnum = resnum
149         self.posY = 0
150
151     def add_text(self, text, bold, href):
152         if href:
153             element = ElementLink(text, href, self.pango_layout)
154             self.element_list.append(element)
155         elif bold:
156             element = ElementBoldText(text, self.pango_layout)
157             self.element_list.append(element)
158         else:
159             element = ElementText(text, self.pango_layout)
160             self.element_list.append(element)
161
162     def get_element_from_xy(self, x, y):
163         for element in self.element_list:
164             if element.is_on_xy(x, y):
165                 return element
166         return None
167
168     def set_width(self, width):
169
170         self.width = width
171         
172         current_x = self.left_margin
173         current_y = 0
174
175         for element in self.element_list:
176             current_x, current_y = element.build_line_list(
177                 current_x, current_y, width, self.left_margin)
178
179         self.height = current_y + Line.HEIGHT
180
181     def get_pixel_size(self):
182         return self.width, self.height
183
184     def draw(self, drawable, x_offset, y_offset):
185
186         for element in self.element_list:
187             element.draw(drawable, y_offset, self.pango_layout)
188
189
190 class ThreadView(gtk.HBox):
191     hand_cursor = gtk.gdk.Cursor(gtk.gdk.HAND2)
192     regular_cursor = gtk.gdk.Cursor(gtk.gdk.XTERM)
193     arrow_cursor = gtk.gdk.Cursor(gtk.gdk.LEFT_PTR)
194
195     def __init__(self):
196         gtk.HBox.__init__(self, False, 0)
197         self.drawingarea = gtk.DrawingArea()
198         self.vscrollbar = gtk.VScrollbar()
199         self.pack_start(self.drawingarea)
200         self.pack_start(self.vscrollbar, expand=False)
201         self.adjustment  = self.vscrollbar.get_adjustment()
202
203         self.drawingarea.add_events(
204             gtk.gdk.SCROLL_MASK |
205             gtk.gdk.POINTER_MOTION_MASK |
206             gtk.gdk.BUTTON_PRESS_MASK |
207             gtk.gdk.BUTTON_RELEASE_MASK)
208
209         self.drawingarea_prev_width = 0
210
211         self.drawingarea.connect(
212             "expose-event", self.on_drawingarea_expose_event)
213         self.drawingarea.connect(
214             "configure-event", self.on_drawingarea_configure_event)
215         self.drawingarea.connect(
216             "scroll-event", self.on_drawingarea_scroll_event)
217         self.drawingarea.connect(
218             "motion-notify-event", self.on_drawingrarea_motion_notify_event)
219         self.drawingarea.connect(
220             "button-press-event", self.on_drawingarea_button_press_event)
221         self.drawingarea.connect(
222             "button-release-event", self.on_drawingarea_button_release_event)
223         self.vscrollbar.connect(
224             "value-changed", self.on_vscrollbar_value_changed)
225
226         self.pango_layout = self.drawingarea.create_pango_layout("")
227
228         self.initialize_buffer()
229
230         self.on_uri_clicked = self._on_uri_clicked
231
232         self.button1_pressed = False
233         self.current_pressed_uri = None
234             
235         self.popupmenu = None
236         self.menu_openuri = None
237         self.menu_copylinkaddress = None
238         self.menu_separator_link = None
239         self.menu_copyselection = None
240         self.menu_openasuri = None
241         self.menu_separator_selection = None
242
243         self.menud_uri = None
244
245     def _on_uri_clicked(self, uri):
246         print uri, "clicked!!!!"
247
248     def initialize_buffer(self):
249         self.res_layout_list = []
250
251     def add_layout(self, res_layout):
252         if (len(self.res_layout_list) != 0):
253             last = self.res_layout_list[len(self.res_layout_list)-1]
254             x, y = last.get_pixel_size()
255             res_layout.posY = last.posY + y
256         self.set_layout_width(res_layout)
257         self.res_layout_list.append(res_layout)
258         x, y = res_layout.get_pixel_size()
259         self.adjustment.upper = res_layout.posY + y
260         self.redraw()
261         self.change_vscrollbar_visible()
262
263     def create_res_layout(self, left_margin, resnum):
264         return ResLayout(left_margin, resnum, self.pango_layout)
265
266     def set_layout_width(self, layout):
267         width = self.drawingarea.allocation.width
268         layout.set_width(width)
269         
270     def redraw(self):
271         self.drawingarea.queue_draw()
272
273     def relayout(self):
274         width = self.drawingarea.allocation.width
275         sum_height = 0
276         for layout in self.res_layout_list:
277             layout.set_width(width)
278             layout.posY = sum_height
279             x, y = layout.get_pixel_size()
280             sum_height += y
281         self.vscrollbar.set_range(0, sum_height)
282         self.change_vscrollbar_visible()
283
284     def change_vscrollbar_visible(self):
285         if self.adjustment.upper < self.adjustment.page_size:
286             self.vscrollbar.hide()
287         else:
288             self.vscrollbar.show()
289
290     def jump(self, value):
291         self.vscrollbar.set_value(value)
292
293     def jump_to_layout(self, layout):
294         self.jump(layout.posY)
295         
296     def jump_to_the_end(self):
297         value = self.adjustment.upper - self.adjustment.page_size
298         self.vscrollbar.set_value(value)
299
300     def jump_to_res(self, resnum):
301         for layout in self.res_layout_list:
302             if layout.resnum == resnum:
303                 self.jump_to_layout(layout)
304                 return True
305         return False
306
307     def draw_viewport(self):
308         view_y = self.vscrollbar.get_value()
309         self.drawingarea.window.draw_rectangle(
310             self.drawingarea.style.base_gc[0],
311             True, 0, 0,
312             self.drawingarea.allocation.width,
313             self.drawingarea.allocation.height)
314
315         gc = self.drawingarea.window.new_gc()
316         for layout in self.res_layout_list:
317             w, h = layout.get_pixel_size()
318             layout_top = layout.posY
319             layout_bottom = layout.posY + h
320             area_top = view_y
321             area_bottom = view_y + self.drawingarea.allocation.height
322             if layout_top <= area_bottom and layout_bottom >= area_top:
323                 layout.draw(self.drawingarea.window,
324                             0, layout.posY - int(view_y))
325
326     def transform_coordinate_gdk_to_adj(self, y):
327         return y + self.vscrollbar.get_value()
328
329     def transform_coordinate_adj_to_layout(self, x, y, layout):
330         return x, y - layout.posY
331
332     def transform_coordinate_gdk_to_layout(self, x, y, layout):
333         return self.transform_coordinate_adj_to_layout(
334             x, self.transform_coordinate_gdk_to_adj(y), layout)
335
336     def ptrpos_to_layout(self, x, y):
337         # transform coordinate, GdkWindow -> adjustment
338         adj_x = x
339         adj_y = self.transform_coordinate_gdk_to_adj(y)
340         for lay in self.res_layout_list:
341             width, height = lay.get_pixel_size()
342             if (adj_y >= lay.posY and adj_y < lay.posY + height and
343                 adj_x >= lay.left_margin):
344                 return lay
345         return None
346
347     def ptrpos_to_uri(self,  x, y):
348         # x, y is GdkWindow coordinate
349
350         layout = self.ptrpos_to_layout(x, y)
351
352         if layout is None:
353             return None, None, None
354
355         # transform coordinate, GdkWindow -> res_layout_list
356         lay_x, lay_y = self.transform_coordinate_gdk_to_layout(x, y, layout)
357
358         # xy -> element
359         element = layout.get_element_from_xy(lay_x, lay_y)
360         if isinstance(element, ElementLink):
361             return element.href, layout, element
362
363         return None, layout, None
364
365     def on_drawingarea_expose_event(self, widget, event, data=None):
366         self.draw_viewport()
367
368     def on_drawingarea_configure_event(self, widget, event, data=None):
369         if event.width != self.drawingarea_prev_width:
370
371             # before relayout, find top layout on gdkwindow
372             top_layout = None
373             delta = 0
374             for lay in self.res_layout_list:
375                 if lay.posY > self.adjustment.value:
376                     break
377                 top_layout = lay
378
379             if top_layout is not None:
380                 delta = top_layout.posY - self.vscrollbar.get_value()
381
382             self.relayout()
383             self.drawingarea_prev_width = event.width
384
385             # after relayout, set vscrollbar.value to top layout's posY
386             if top_layout is not None:
387                 self.vscrollbar.set_value(top_layout.posY - delta)
388
389         self.adjustment.page_size = self.drawingarea.allocation.height
390         self.vscrollbar.set_increments(20, self.drawingarea.allocation.height)
391
392         # re-set 'value' for prevent overflow
393         self.vscrollbar.set_value(self.vscrollbar.get_value())
394         self.change_vscrollbar_visible()
395
396     def on_vscrollbar_value_changed(self, widget, data=None):
397         self.drawingarea.queue_draw()
398
399     def on_drawingarea_scroll_event(self, widget, event, data=None):
400         self.vscrollbar.emit("scroll-event", event)
401
402     def on_drawingrarea_motion_notify_event(self, widget, event, data=None):
403         if event.state & gtk.gdk.BUTTON1_MASK != gtk.gdk.BUTTON1_MASK:
404             self.button1_pressed = False
405
406         cursor = ThreadView.regular_cursor
407
408         uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
409         if layout is None:
410             cursor = ThreadView.arrow_cursor
411         else:
412             if uri is not None and uri != "":
413                 cursor = ThreadView.hand_cursor
414
415         self.drawingarea.window.set_cursor(cursor)
416
417     def on_drawingarea_button_press_event(self, widget, event, data=None):
418         if event.button == 1:
419             self.current_pressed_uri = None
420             self.button1_pressed = True
421             uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
422             if uri is not None and layout is not None and element is not None:
423                 self.current_pressed_uri = (uri, layout, element)
424         elif event.button == 3:
425             time = event.time
426             uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
427             if uri is not None and layout is not None and element is not None:
428                 self.menu_openuri.show()
429                 self.menu_copylinkaddress.show()
430                 self.menu_separator_link.show()
431                 self.menu_openuri.uri = uri
432                 self.menu_copylinkaddress.uri = uri
433             else:
434                 self.menu_openuri.hide()
435                 self.menu_copylinkaddress.hide()
436                 self.menu_separator_link.hide()
437                 self.menu_openuri.uri = None
438                 self.menu_copylinkaddress.uri = None
439
440             self.menu_copyselection.hide()
441             self.menu_openasuri.hide()
442             self.menu_separator_selection.hide()
443
444             self.popupmenu.popup(None, None, None, event.button, time)
445             return True
446             
447
448     def on_drawingarea_button_release_event(self, widget, event, data=None):
449         if event.button == 1:
450             button1_pressed = self.button1_pressed
451             self.button1_pressed = False
452
453             if button1_pressed and self.current_pressed_uri is not None:
454                 uri, layout, element = self.ptrpos_to_uri(event.x, event.y)
455                 p_uri, p_layout, p_element = self.current_pressed_uri
456                 self.current_preesed_uri = None
457                 if (uri == p_uri and layout == p_layout and
458                     element == p_element):
459                     self.on_uri_clicked(uri)