# Copyright (C) 2010 Matthew McGowan
#
# Authors:
# Matthew McGowan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from gi.repository import Gtk, Gdk
from gi.repository import GLib
from gi.repository import Pango
from softwarecenter.utils import normalize_package_description
from softwarecenter.ui.gtk3.drawing import color_to_hex
from softwarecenter.ui.gtk3.utils import point_in
_PS = Pango.SCALE
class _SpecialCasePreParsers(object):
def preparse(self, k, desc):
if k is None:
return desc
func_name = '_%s_preparser' % k.lower().replace('-', '_')
if not hasattr(self, func_name):
return desc
f = getattr(self, func_name)
return f(desc)
# special case pre-parsers
def _skype_preparser(self, desc):
return desc.replace('. *', '.\n*')
def _texlive_fonts_extra_preparser(self, desc):
return desc.replace(')\n', ').\n').replace('--\n', '--\n\n')
class EventHelper(dict):
# FIXME: workaround for broken event.copy()
class ButtonEvent(object):
def __init__(self, event):
self.x = event.x
self.y = event.y
self.type = event.type
self.button = event.button
VALID_KEYS = (
'event',
'layout',
'index',
'within-selection',
'drag-active',
'drag-context')
def __init__(self):
dict.__init__(self)
self.new_press(None, None, None, False)
def __setitem__(self, k, v):
if k not in EventHelper.VALID_KEYS:
raise KeyError('\"%s\" is not a valid key' % k)
return False
return dict.__setitem__(self, k, v)
def new_press(self, event, layout, index, within_sel):
if event is None:
self['event'] = None
else:
# this should be simply event.copy() but that appears broken
# currently(?)
self['event'] = EventHelper.ButtonEvent(event)
self['layout'] = layout
self['index'] = index
self['within-selection'] = within_sel
self['drag-active'] = False
self['drag-context'] = None
class PangoLayoutProxy(object):
""" Because i couldn't figure out how to inherit from
pygi's Pango.Layout... """
def __init__(self, context):
self._layout = Pango.Layout.new(context)
def xy_to_index(self, x, y):
return self._layout.xy_to_index(x, y)
def index_to_pos(self, *args):
return self._layout.index_to_pos(*args)
# setter proxies
def set_attributes(self, attrs):
return self._layout.set_attributes(attrs)
def set_markup(self, markup):
return self._layout.set_markup(markup, -1)
def set_font_description(self, font_desc):
return self._layout.set_font_description(font_desc)
def set_wrap(self, wrap_mode):
return self._layout.set_wrap(wrap_mode)
def set_width(self, width):
return self._layout.set_width(width)
# getter proxies
def get_text(self):
return self._layout.get_text()
def get_pixel_extents(self):
return self._layout.get_pixel_extents()[1]
def get_cursor_pos(self, index):
return self._layout.get_cursor_pos(index)
def get_iter(self):
return self._layout.get_iter()
def get_extents(self):
return self._layout.get_extents()
class Layout(PangoLayoutProxy):
def __init__(self, widget, text=""):
PangoLayoutProxy.__init__(self, widget.get_pango_context())
self.widget = widget
self.length = 0
self.indent = 0
self.vspacing = None
self.is_bullet = False
self.index = 0
self.allocation = Gdk.Rectangle()
self._default_attrs = True
self.set_markup(text)
def __len__(self):
return self.length
def set_text(self, text):
PangoLayoutProxy.set_markup(self, text)
self.length = len(self.get_text())
def set_allocation(self, x, y, w, h):
a = self.allocation
a.x = x
a.y = y
a.width = w
a.height = h
def get_position(self):
return self.allocation.x, self.allocation.y
def cursor_up(self, cursor, target_x=-1):
layout = self.widget.order[cursor.paragraph]
pos = layout.index_to_pos(cursor.index)
x, y = pos.x, pos.y
if target_x >= 0:
x = target_x
y -= _PS * self.widget.line_height
return layout.xy_to_index(x, y), (x, y)
def cursor_down(self, cursor, target_x=-1):
layout = self.widget.order[cursor.paragraph]
pos = layout.index_to_pos(cursor.index)
x, y = pos.x, pos.y
if target_x >= 0:
x = target_x
y += _PS * self.widget.line_height
return layout.xy_to_index(x, y), (x, y)
def index_at(self, px, py):
#wa = self.widget.get_allocation()
x, y = self.get_position() # layout allocation
(_, index, k) = self.xy_to_index((px - x) * _PS, (py - y) * _PS)
return point_in(self.allocation, px, py), index + k
def reset_attrs(self):
#~ self.set_attributes(Pango.AttrList())
self.set_markup(self.get_text())
self._default_attrs = True
def highlight(self, start, end, bg, fg):
# FIXME: AttrBackground doesnt seem to be expose by gi yet??
#~ attrs = Pango.AttrList()
#~ attrs.insert(Pango.AttrBackground(bg.red, bg.green, bg.blue, start,
#~ end))
#~ attrs.insert(Pango.AttrForeground(fg.red, fg.green, fg.blue, start,
#~ end))
#~ self.set_attributes(attrs)
# XXX: workaround
text = self.get_text()
new_text = (text[:start] + '' %
(bg, fg))
new_text += text[start:end]
new_text += '' + text[end:]
self.set_markup(new_text)
self._default_attrs = False
def highlight_all(self, bg, fg):
# FIXME: AttrBackground doesnt seem to be expose by gi yet??
#~ attrs = Pango.AttrList()
#~ attrs.insert(Pango.AttrBackground(bg.red, bg.green, bg.blue, 0, -1))
#~ attrs.insert(Pango.AttrForeground(fg.red, fg.green, fg.blue, 0, -1))
#~ self.set_attributes(attrs)
# XXX: workaround
text = self.get_text()
self.set_markup('%s' %
(bg, fg, text))
self._default_attrs = False
class Cursor(object):
WORD_TERMINATORS = (' ',) # empty space. suggestions recommended...
def __init__(self, parent):
self.parent = parent
self.index = 0
self.paragraph = 0
def is_min(self, cursor):
return self.get_position() <= cursor.get_position()
def is_max(self, cursor):
return self.get_position() >= cursor.get_position()
def switch(self, cursor):
this_pos = self.get_position()
other_pos = cursor.get_position()
self.set_position(*other_pos)
cursor.set_position(*this_pos)
def same_line(self, cursor):
return self.get_current_line()[0] == cursor.get_current_line()[0]
def get_current_line(self):
keep_going = True
i, it = self.index, self.parent.order[self.paragraph].get_iter()
ln = 0
while keep_going:
l = it.get_line()
ls = l.start_index
le = ls + l.length
if i >= ls and i <= le:
if not it.at_last_line():
le -= 1
return (self.paragraph, ln), (ls, le)
ln += 1
keep_going = it.next_line()
return None, None, None
def get_current_word(self):
keep_going = True
layout = self.parent.order[self.paragraph]
text = layout.get_text()
i, it = self.index, layout.get_iter()
start = 0
while keep_going:
j = it.get_index()
if j >= i and text[j] in self.WORD_TERMINATORS:
return self.paragraph, (start, j)
elif text[j] in self.WORD_TERMINATORS:
start = j + 1
keep_going = it.next_char()
return self.paragraph, (start, len(layout))
def set_position(self, paragraph, index):
self.index = index
self.paragraph = paragraph
def get_position(self):
return self.paragraph, self.index
class PrimaryCursor(Cursor):
def __init__(self, parent):
Cursor.__init__(self, parent)
def __repr__(self):
return 'Cursor: ' + str((self.paragraph, self.index))
def get_rectangle(self, layout, a):
if self.index < len(layout):
pos = layout.get_cursor_pos(self.index)[1]
else:
pos = layout.get_cursor_pos(len(layout))[1]
x = layout.allocation.x + pos.x / _PS
y = layout.allocation.y + pos.y / _PS
return x, y, 1, pos.height / _PS
def draw(self, cr, layout, a):
cr.set_source_rgb(0, 0, 0)
cr.rectangle(*self.get_rectangle(layout, a))
cr.fill()
def zero(self):
self.index = 0
self.paragraph = 0
class SelectionCursor(Cursor):
def __init__(self, cursor):
Cursor.__init__(self, cursor.parent)
self.cursor = cursor
self.target_x = None
self.target_x_indent = 0
self.restore_point = None
def __repr__(self):
return 'Selection: ' + str(self.get_range())
def __nonzero__(self):
c = self.cursor
return (self.paragraph, self.index) != (c.paragraph, c.index)
@property
def min(self):
c = self.cursor
return min((self.paragraph, self.index), (c.paragraph, c.index))
@property
def max(self):
c = self.cursor
return max((self.paragraph, self.index), (c.paragraph, c.index))
def clear(self, key=None):
self.index = self.cursor.index
self.paragraph = self.cursor.paragraph
self.restore_point = None
if key not in (Gdk.KEY_uparrow, Gdk.KEY_downarrow):
self.target_x = None
self.target_x_indent = 0
def set_target_x(self, x, indent):
self.target_x = x
self.target_x_indent = indent
def get_range(self):
return self.min, self.max
def within_selection(self, pos):
l = list(self.get_range())
l.append(pos)
l.sort()
# sort the list, see if pos is in between the extents of the selection
# range, if it is, pos is within the selection
if pos in l:
return l.index(pos) == 1
return False
class TextBlock(Gtk.EventBox):
PAINT_PRIMARY_CURSOR = False
DEBUG_PAINT_BBOXES = False
BULLET_POINT = u' \u2022 '
def __init__(self):
Gtk.EventBox.__init__(self)
self.set_visible_window(False)
self.set_size_request(200, -1)
self.set_can_focus(True)
self.set_events(Gdk.EventMask.KEY_PRESS_MASK |
Gdk.EventMask.ENTER_NOTIFY_MASK |
Gdk.EventMask.LEAVE_NOTIFY_MASK |
Gdk.EventMask.BUTTON_RELEASE_MASK |
Gdk.EventMask.POINTER_MOTION_MASK)
self._is_new = False
self.order = []
self.cursor = cur = PrimaryCursor(self)
self.selection = sel = SelectionCursor(self.cursor)
self.clipboard = None
#~ event_helper = EventHelper()
self._update_cached_layouts()
self._test_layout = self.create_pango_layout('')
#self._xterm = Gdk.Cursor.new(Gdk.XTERM)
# popup menu and menuitems
self.copy_menuitem = Gtk.ImageMenuItem.new_from_stock(
Gtk.STOCK_COPY, None)
self.select_all_menuitem = Gtk.ImageMenuItem.new_from_stock(
Gtk.STOCK_SELECT_ALL, None)
self.menu = Gtk.Menu()
self.menu.attach_to_widget(self, None)
self.menu.append(self.copy_menuitem)
self.menu.append(self.select_all_menuitem)
self.menu.show_all()
self.copy_menuitem.connect('select', self._menu_do_copy, sel)
self.select_all_menuitem.connect('select', self._menu_do_select_all,
cur, sel)
#~ Gtk.drag_source_set(self, Gdk.ModifierType.BUTTON1_MASK,
#~ None, Gdk.DragAction.COPY)
#~ Gtk.drag_source_add_text_targets(self)
#~ self.connect('drag-begin', self._on_drag_begin)
#~ self.connect('drag-data-get', self._on_drag_data_get, sel)
event_helper = EventHelper()
self.connect('button-press-event', self._on_press, event_helper, cur,
sel)
self.connect('button-release-event', self._on_release, event_helper,
cur, sel)
self.connect('motion-notify-event', self._on_motion, event_helper,
cur, sel)
self.connect('key-press-event', self._on_key_press, cur, sel)
self.connect('key-release-event', self._on_key_release, cur, sel)
self.connect('focus-in-event', self._on_focus_in)
self.connect('focus-out-event', self._on_focus_out)
self.connect("size-allocate", self.on_size_allocate)
self.connect('style-updated', self._on_style_updated)
# workaround broken engines (LP: #1021308)
self.emit("style-updated")
def on_size_allocate(self, *args):
allocation = self.get_allocation()
width = allocation.width
x = y = 0
for layout in self.order:
layout.set_width(_PS * (width - layout.indent))
if layout.index > 0:
y += (layout.vspacing or self.line_height)
e = layout.get_pixel_extents()
if self.get_direction() != Gtk.TextDirection.RTL:
layout.set_allocation(e.x + layout.indent, y + e.y,
width - layout.indent, e.height)
else:
layout.set_allocation(x + width - e.x - e.width -
layout.indent - 1, y + e.y, width - layout.indent,
e.height)
y += e.y + e.height
# overrides
def do_get_request_mode(self):
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH
def do_get_preferred_height_for_width(self, width):
height = 0
layout = self._test_layout
for l in self.order:
layout.set_text(l.get_text(), -1)
layout.set_width(_PS * (width - l.indent))
lh = layout.get_pixel_extents()[1].height
height += lh + (l.vspacing or self.line_height)
height = max(50, height)
return height, height
def do_draw(self, cr):
self.render(self, cr)
def _config_colors(self):
context = self.get_style_context()
context.save()
context.add_class(Gtk.STYLE_CLASS_HIGHLIGHT)
state = self.get_state_flags()
if self.has_focus():
state |= Gtk.StateFlags.FOCUSED
context.set_state(state)
self._bg = color_to_hex(context.get_background_color(state))
self._fg = color_to_hex(context.get_color(state))
context.restore()
def _on_style_updated(self, widget):
self._config_colors()
self._update_cached_layouts()
# def _on_drag_begin(self, widgets, context, event_helper):
# print 'drag: begin'
def _on_drag_data_get(self, widget, context, selection, info, timestamp,
sel):
# print 'drag: get data'
text = self.get_selected_text(sel)
selection.set_text(text, -1)
def _on_focus_in(self, widget, event):
self._config_colors()
def _on_focus_out(self, widget, event):
self._config_colors()
def _on_motion(self, widget, event, event_helper, cur, sel):
if not (event.state == Gdk.ModifierType.BUTTON1_MASK):
# or not self.has_focus():
return
# check if we have moved enough to count as a drag
press = event_helper['event']
# mvo: how can this be?
if not press:
return
start_x, start_y = int(press.x), int(press.y)
cur_x, cur_y = int(event.x), int(event.y)
if (not event_helper['drag-active'] and
self.drag_check_threshold(start_x, start_y, cur_x, cur_y)):
event_helper['drag-active'] = True
if not event_helper['drag-active']:
return
#~ if (event_helper['within-selection'] and
#~ not event_helper['drag-context']):
#~ target_list = Gtk.TargetList()
#~ target_list.add_text_targets(80)
#~ ctx = self.drag_begin(target_list, # target list
#~ Gdk.DragAction.COPY, # action
#~ 1, # initiating button
#~ event) # event
#~
#~ event_helper['drag-context'] = ctx
#~ return
for layout in self.order:
point_in, index = layout.index_at(cur_x, cur_y)
if point_in:
cur.set_position(layout.index, index)
self.queue_draw()
break
def _on_press(self, widget, event, event_helper, cur, sel):
if sel and not self.has_focus():
self.grab_focus()
return # spot the difference
if not self.has_focus():
self.grab_focus()
if event.button == 3:
self._button3_action(cur, sel, event)
return
elif event.button != 1:
return
for layout in self.order:
x, y = int(event.x), int(event.y)
point_in, index = layout.index_at(x, y)
if point_in:
within_sel = False
#~ within_sel = sel.within_selection((layout.index, index))
if not within_sel:
cur.set_position(layout.index, index)
sel.clear()
#~ event_helper.new_press(event.copy(), layout, index,
#~ within_sel)
event_helper.new_press(event, layout, index, within_sel)
break
def _on_release(self, widget, event, event_helper, cur, sel):
if not event_helper['event']:
return
# check if a drag occurred
if event_helper['drag-active']:
# if so, do not handle release
return
# else, handle release, do click
cur.set_position(event_helper['layout'].index,
event_helper['index'])
sel.clear()
press = event_helper['event']
if (press.type == Gdk.EventType._2BUTTON_PRESS):
self._2click_select(cur, sel)
elif (press.type == Gdk.EventType._3BUTTON_PRESS):
self._3click_select(cur, sel)
self.queue_draw()
def _menu_do_copy(self, item, sel):
self._copy_text(sel)
def _menu_do_select_all(self, item, cur, sel):
self._select_all(cur, sel)
def _button3_action(self, cur, sel, event):
start, end = sel.get_range()
self.copy_menuitem.set_sensitive(True)
self.select_all_menuitem.set_sensitive(True)
if not sel:
self.copy_menuitem.set_sensitive(False)
elif (start == (0, 0) and
end == (len(self.order) - 1, len(self.order[-1]))):
self.select_all_menuitem.set_sensitive(False)
self.menu.popup(None, # parent_menu_shell,
None, # parent_menu_item,
None, # GtkMenuPositionFunc func,
None, # data,
event.button,
event.time)
def _on_key_press(self, widget, event, cur, sel):
kv = event.keyval
s, i = cur.paragraph, cur.index
handled_keys = True
ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK) > 0
shift = (event.state & Gdk.ModifierType.SHIFT_MASK) > 0
if (not self.PAINT_PRIMARY_CURSOR and
kv in (Gdk.KEY_uparrow, Gdk.KEY_downarrow) and
not sel):
return False
if kv == Gdk.KEY_Tab:
handled_keys = False
elif kv == Gdk.KEY_Left:
if ctrl:
self._select_left_word(cur, sel, s, i)
else:
self._select_left(cur, sel, s, i, shift)
if shift:
layout = self._get_cursor_layout()
pos = layout.index_to_pos(cur.index)
sel.set_target_x(pos.x, layout.indent)
elif kv == Gdk.KEY_Right:
if ctrl:
self._select_right_word(cur, sel, s, i)
else:
self._select_right(cur, sel, s, i, shift)
if shift:
layout = self._get_cursor_layout()
pos = layout.index_to_pos(cur.index)
sel.set_target_x(pos.x, layout.indent)
elif kv == Gdk.KEY_Up:
if ctrl:
if i == 0:
if s > 0:
cur.paragraph -= 1
cur.set_position(cur.paragraph, 0)
elif sel and not shift:
cur.set_position(*sel.min)
else:
self._select_up(cur, sel)
elif kv == Gdk.KEY_Down:
if ctrl:
if i == len(self._get_layout(cur)):
if s + 1 < len(self.order):
cur.paragraph += 1
i = len(self._get_layout(cur))
cur.set_position(cur.paragraph, i)
elif sel and not shift:
cur.set_position(*sel.max)
else:
self._select_down(cur, sel)
elif kv == Gdk.KEY_Home:
if shift:
self._select_home(cur, sel, self.order[cur.paragraph])
else:
cur.set_position(0, 0)
elif kv == Gdk.KEY_End:
if shift:
self._select_end(cur, sel, self.order[cur.paragraph])
else:
cur.paragraph = len(self.order) - 1
cur.index = len(self._get_layout(cur))
else:
handled_keys = False
if not shift and handled_keys:
sel.clear(kv)
self.queue_draw()
return handled_keys
def _on_key_release(self, widget, event, cur, sel):
ctrl = (event.state & Gdk.ModifierType.CONTROL_MASK) > 0
if ctrl:
if event.keyval == Gdk.KEY_a:
self._select_all(cur, sel)
elif event.keyval == Gdk.KEY_c:
self._copy_text(sel)
self.queue_draw()
def _select_up(self, cur, sel):
#~ if sel and not cur.is_min(sel) and cur.same_line(sel):
#~ cur.switch(sel)
s = cur.paragraph
layout = self._get_layout(cur)
if sel.target_x:
x = sel.target_x
if sel.target_x_indent:
x += (sel.target_x_indent - layout.indent) * _PS
(_, j, k), (x, y) = layout.cursor_up(cur, x)
j += k
else:
(_, j, k), (x, y) = layout.cursor_up(cur)
j += k
sel.set_target_x(x, layout.indent)
if (s, j) != cur.get_position():
cur.set_position(s, j)
elif s > 0:
cur.paragraph = s - 1
layout = self._get_layout(cur)
if sel.target_x_indent:
x += (sel.target_x_indent - layout.indent) * _PS
y = layout.get_extents()[0].height
(_, j, k) = layout.xy_to_index(x, y)
cur.set_position(s - 1, j + k)
else:
return False
return True
def _select_down(self, cur, sel):
#~ if sel and not cur.is_max(sel) and cur.same_line(sel):
#~ cur.switch(sel)
s = cur.paragraph
layout = self._get_layout(cur)
if sel.target_x:
x = sel.target_x
if sel.target_x_indent:
x += (sel.target_x_indent - layout.indent) * _PS
(_, j, k), (x, y) = layout.cursor_down(cur, x)
j += k
else:
(_, j, k), (x, y) = layout.cursor_down(cur)
j += k
sel.set_target_x(x, layout.indent)
if (s, j) != cur.get_position():
cur.set_position(s, j)
elif s < len(self.order) - 1:
cur.paragraph = s + 1
layout = self._get_layout(cur)
if sel.target_x_indent:
x += (sel.target_x_indent - layout.indent) * _PS
y = 0
(_, j, k) = layout.xy_to_index(x, y)
cur.set_position(s + 1, j + k)
else:
return False
return True
def _2click_select(self, cursor, sel):
self._select_word(cursor, sel)
def _3click_select(self, cursor, sel):
# XXX:
# _select_line seems to expose the following Pango issue:
# (description.py:3892): Pango-CRITICAL **:
# pango_layout_line_unref: assertion `private->ref_count > 0'
# failed
# ... which can result in a segfault
#~ self._select_line(cursor, sel)
self._select_all(cursor, sel)
def _copy_text(self, sel):
text = self.get_selected_text(sel)
if not self.clipboard:
display = Gdk.Display.get_default()
selection = Gdk.Atom.intern("CLIPBOARD", False)
self.clipboard = Gtk.Clipboard.get_for_display(display, selection)
self.clipboard.clear()
self.clipboard.set_text(text.strip(), -1)
def _select_end(self, cur, sel, layout):
if not cur.is_max(sel):
cur.switch(sel)
n, r, line = cur.get_current_line()
cur_pos = cur.get_position()
if cur_pos == (len(self.order) - 1, len(self.order[-1])): # abs end
if sel.restore_point:
# reinstate restore point
cur.set_position(*sel.restore_point)
else:
# reselect the line end
n, r, line = sel.get_current_line()
cur.set_position(n[0], r[1])
elif cur_pos[1] == len(self.order[n[0]]): # para end
# select abs end
cur.set_position(len(self.order) - 1, len(self.order[-1]))
elif cur_pos == (n[0], r[1]): # line end
# select para end
cur.set_position(n[0], len(self.order[n[0]]))
else: # not at any end, within line somewhere
# select line end
if sel:
sel.restore_point = cur_pos
cur.set_position(n[0], r[1])
def _select_home(self, cur, sel, layout):
if not cur.is_min(sel):
cur.switch(sel)
n, r, line = cur.get_current_line()
cur_pos = cur.get_position()
if cur_pos == (0, 0): # absolute home
if sel.restore_point:
cur.set_position(*sel.restore_point)
else:
n, r, line = sel.get_current_line()
cur.set_position(n[0], r[0])
elif cur_pos[1] == 0: # para home
cur.set_position(0, 0)
elif cur_pos == (n[0], r[0]): # line home
cur.set_position(n[0], 0)
else: # not at any home, within line somewhere
if sel:
sel.restore_point = cur_pos
cur.set_position(n[0], r[0])
def _select_left(self, cur, sel, s, i, shift):
if not shift and not cur.is_min(sel):
cur.switch(sel)
return
if i > 0:
cur.set_position(s, i - 1)
elif cur.paragraph > 0:
cur.paragraph -= 1
cur.set_position(s - 1, len(self._get_layout(cur)))
def _select_right(self, cur, sel, s, i, shift):
if not shift and not cur.is_max(sel):
cur.switch(sel)
return
if i < len(self._get_layout(cur)):
cur.set_position(s, i + 1)
elif s < len(self.order) - 1:
cur.set_position(s + 1, 0)
def _select_left_word(self, cur, sel, s, i):
if i > 0:
cur.index -= 1
elif s > 0:
cur.paragraph -= 1
cur.index = len(self._get_layout(cur))
paragraph, word = cur.get_current_word()
if not word:
return
cur.set_position(paragraph, max(0, word[0] - 1))
def _select_right_word(self, cur, sel, s, i):
ll = len(self._get_layout(cur))
if i < ll:
cur.index += 1
elif s + 1 < len(self.order):
cur.paragraph += 1
cur.index = 0
paragraph, word = cur.get_current_word()
if not word:
return
cur.set_position(paragraph, min(word[1] + 1, ll))
def _select_word(self, cursor, sel):
paragraph, word = cursor.get_current_word()
if word:
cursor.set_position(paragraph, word[1] + 1)
sel.set_position(paragraph, word[0])
if self.get_direction() == Gtk.TextDirection.RTL:
cursor.switch(sel)
def _select_line(self, cursor, sel):
n, r = self.cursor.get_current_line()
sel.set_position(n[0], r[0])
cursor.set_position(n[0], r[1])
if self.get_direction() == Gtk.TextDirection.RTL:
cursor.switch(sel)
def _select_all(self, cursor, sel):
layout = self.order[-1]
sel.set_position(0, 0)
cursor.set_position(layout.index, len(layout))
if self.get_direction() == Gtk.TextDirection.RTL:
cursor.switch(sel)
def _selection_copy(self, layout, sel, new_para=True):
i = layout.index
start, end = sel.get_range()
if new_para:
text = '\n\n'
else:
text = ''
if sel and i >= start[0] and i <= end[0]:
if i == start[0]:
if end[0] > i:
return text + layout.get_text()[start[1]: len(layout)]
else:
return text + layout.get_text()[start[1]: end[1]]
elif i == end[0]:
if start[0] < i:
return text + layout.get_text()[0: end[1]]
else:
return text + layout.get_text()[start[1]: end[1]]
else:
return text + layout.get_text()
return ''
def _new_layout(self, text=''):
layout = Layout(self, text)
layout.set_wrap(Pango.WrapMode.WORD_CHAR)
return layout
def _update_cached_layouts(self):
self._bullet = self._new_layout()
self._bullet.set_markup(self.BULLET_POINT)
font_desc = Pango.FontDescription()
font_desc.set_weight(Pango.Weight.BOLD)
self._bullet.set_font_description(font_desc)
e = self._bullet.get_pixel_extents()
self.indent, self.line_height = e.width, e.height
def _selection_highlight(self, layout, sel, bg, fg):
i = layout.index
start, end = sel.get_range()
if sel and i >= start[0] and i <= end[0]:
if i == start[0]:
if end[0] > i:
layout.highlight(start[1], len(layout), bg, fg)
else:
layout.highlight(start[1], end[1], bg, fg)
elif i == end[0]:
if start[0] < i:
layout.highlight(0, end[1], bg, fg)
else:
layout.highlight(start[1], end[1], bg, fg)
else:
layout.highlight_all(bg, fg)
elif not layout._default_attrs:
layout.reset_attrs()
def _paint_bullet_point(self, cr, x, y):
# draw the layout
Gtk.render_layout(self.get_style_context(),
cr, # state
x, # x coord
y, # y coord
self._bullet._layout) # a Pango.Layout()
def _get_layout(self, cursor):
return self.order[cursor.paragraph]
def _get_cursor_layout(self):
return self.order[self.cursor.paragraph]
def _get_selection_layout(self):
return self.order[self.selection.paragraph]
def render(self, widget, cr):
if not self.order:
return
cr.save()
a = self.get_allocation()
for layout in self.order:
lx, ly = layout.get_position()
self._selection_highlight(layout,
self.selection,
self._bg, self._fg)
if layout.is_bullet:
if self.get_direction() != Gtk.TextDirection.RTL:
indent = layout.indent - self.indent
else:
indent = a.width - layout.indent
self._paint_bullet_point(cr, indent, ly)
if self.DEBUG_PAINT_BBOXES:
la = layout.allocation
cr.rectangle(la.x, la.y, la.width, la.height)
cr.set_source_rgb(1, 0, 0)
cr.stroke()
# draw the layout
Gtk.render_layout(self.get_style_context(),
cr,
lx, # x coord
ly, # y coord
layout._layout) # a Pango.Layout()
# draw the cursor
if self.PAINT_PRIMARY_CURSOR and self.has_focus():
self.cursor.draw(cr, self._get_layout(self.cursor), a)
cr.restore()
def append_paragraph(self, p, vspacing=None):
l = self._new_layout()
l.index = len(self.order)
l.vspacing = vspacing
l.set_text(p)
self.order.append(l)
def append_bullet(self, point, indent_level, vspacing=None):
l = self._new_layout()
l.index = len(self.order)
l.indent = self.indent * (indent_level + 1)
l.vspacing = vspacing
l.is_bullet = True
l.set_text(point)
self.order.append(l)
def copy_clipboard(self):
self._copy_text(self.selection)
def get_selected_text(self, sel=None):
text = ''
if not sel:
sel = self.selection
for layout in self.order:
text += self._selection_copy(layout, sel, (layout.index > 0))
return text
def select_all(self):
self._select_all(self.cursor, self.selection)
self.queue_draw()
def finished(self):
self.queue_resize()
def clear(self, key=None):
self.cursor.zero()
self.selection.clear(key)
self.order = []
class AppDescription(Gtk.VBox):
TYPE_PARAGRAPH = 0
TYPE_BULLET = 1
_preparser = _SpecialCasePreParsers()
def __init__(self):
Gtk.VBox.__init__(self)
self.description = TextBlock()
self.pack_start(self.description, False, False, 0)
self._prev_type = None
def _part_is_bullet(self, part):
# normalize_description() ensures that we only have "* " bullets
i = part.find("* ")
return i > -1, i
def _parse_desc(self, desc, pkgname):
""" Attempt to maintain original fixed width layout, while
reconstructing the description into text blocks
(either paragraphs or bullets) which are line-wrap friendly.
"""
# pre-parse description if special case exists for the given pkgname
desc = self._preparser.preparse(pkgname, desc)
parts = normalize_package_description(desc).split('\n')
for part in parts:
if not part:
continue
is_bullet, indent = self._part_is_bullet(part)
if is_bullet:
self.append_bullet(part, indent)
else:
self.append_paragraph(part)
self.description.finished()
def clear(self):
self.description.clear()
def append_paragraph(self, p):
vspacing = self.description.line_height
self.description.append_paragraph(p.strip(), vspacing)
self._prev_type = self.TYPE_PARAGRAPH
def append_bullet(self, point, indent_level):
if self._prev_type == self.TYPE_BULLET:
vspacing = int(0.4 * self.description.line_height)
else:
vspacing = self.description.line_height
self.description.append_bullet(
point[indent_level + 2:], indent_level, vspacing)
self._prev_type = self.TYPE_BULLET
def set_description(self, raw_desc, pkgname):
self.clear()
if type(raw_desc) == str:
encoded_desc = unicode(raw_desc, 'utf8').encode('utf8')
else:
encoded_desc = raw_desc.encode('utf8')
self._text = GLib.markup_escape_text(encoded_desc)
self._parse_desc(self._text, pkgname)
self.show_all()
# easy access to some TextBlock methods
def copy_clipboard(self):
return TextBlock.copy_clipboard(self.description)
def get_selected_text(self):
return TextBlock.get_selected_text(self.description)
def select_all(self):
return TextBlock.select_all(self.description)