1# -*- coding: utf-8 -*- 2# 3# info.py - commander 4# 5# Copyright (C) 2010 - Jesse van den Kieboom 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin Street, Fifth Floor, 20# Boston, MA 02110-1301, USA. 21 22from gi.repository import Pango, Gdk, Gtk 23import math 24 25class ScrolledWindow(Gtk.ScrolledWindow): 26 __gtype_name__ = "CommanderScrolledWindow" 27 28 def __init__(self): 29 Gtk.ScrolledWindow.__init__(self) 30 31 self._max_height = 0 32 self._max_lines = 10 33 34 self.view = Gtk.TextView() 35 self.view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) 36 self.view.set_editable(False) 37 self.view.set_can_focus(False) 38 39 self.view.connect('style-updated', self._on_style_updated) 40 41 self._update_max_height() 42 43 self.view.show() 44 45 self.add(self.view) 46 47 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) 48 49 def _update_max_height(self): 50 layout = self.view.create_pango_layout('Some text to measure') 51 extents = layout.get_pixel_extents() 52 53 maxh = extents[1].height * self._max_lines 54 55 if maxh != self._max_height: 56 self._max_height = maxh 57 self.queue_resize() 58 59 def _on_style_updated(self, widget): 60 self._update_max_height() 61 62 def do_get_preferred_height(self): 63 hp, vp = self.get_policy() 64 65 ret = self.view.get_preferred_height() 66 67 if vp == Gtk.PolicyType.NEVER and ret[0] > self._max_height: 68 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS) 69 self.set_min_content_height(self._max_height) 70 elif vp == Gtk.PolicyType.ALWAYS and ret[0] < self._max_height: 71 self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER) 72 self.set_min_content_height(0) 73 74 return Gtk.ScrolledWindow.do_get_preferred_height(self) 75 76class Info(Gtk.Box): 77 __gtype_name__ = "CommanderInfo" 78 79 def __init__(self): 80 super(Info, self).__init__() 81 82 self._button_bar = None 83 self._status_label = None 84 85 self._build_ui() 86 87 def _build_ui(self): 88 self.set_orientation(Gtk.Orientation.VERTICAL) 89 self.set_spacing(3) 90 self.set_can_focus(False) 91 92 self._sw = ScrolledWindow() 93 self._sw.set_border_width(6) 94 95 css = Gtk.CssProvider() 96 css.load_from_data(bytes(""" 97.trough { 98 background: transparent; 99} 100""", 'utf-8')) 101 102 self._sw.get_vscrollbar().get_style_context().add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 103 104 self._sw.show() 105 self.add(self._sw) 106 107 self._attr_map = { 108 Pango.AttrType.STYLE: ('style', Pango.AttrInt), 109 Pango.AttrType.WEIGHT: ('weight', Pango.AttrInt), 110 Pango.AttrType.VARIANT: ('variant', Pango.AttrInt), 111 Pango.AttrType.STRETCH: ('stretch', Pango.AttrInt), 112 Pango.AttrType.SIZE: ('size', Pango.AttrInt), 113 Pango.AttrType.FOREGROUND: ('foreground', Pango.AttrColor), 114 Pango.AttrType.BACKGROUND: ('background', Pango.AttrColor), 115 Pango.AttrType.UNDERLINE: ('underline', Pango.AttrInt), 116 Pango.AttrType.STRIKETHROUGH: ('strikethrough', Pango.AttrInt), 117 Pango.AttrType.RISE: ('rise', Pango.AttrInt), 118 Pango.AttrType.SCALE: ('scale', Pango.AttrFloat) 119 } 120 121 @property 122 def text_view(self): 123 return self._sw.view 124 125 @property 126 def is_empty(self): 127 buf = self.text_view.get_buffer() 128 return buf.get_start_iter().equal(buf.get_end_iter()) 129 130 def status(self, text=None): 131 if self._status_label == None and text != None: 132 self._status_label = Gtk.Label() 133 134 context = self.text_view.get_style_context() 135 state = context.get_state() 136 font_desc = context.get_font(state) 137 138 self._status_label.override_font(font_desc) 139 140 self._status_label.override_color(Gtk.StateFlags.NORMAL, context.get_color(Gtk.StateFlags.NORMAL)) 141 self._status_label.show() 142 self._status_label.set_alignment(0, 0.5) 143 self._status_label.set_padding(10, 0) 144 self._status_label.set_use_markup(True) 145 self._status_label.set_halign(Gtk.Align.FILL) 146 147 self._ensure_button_bar() 148 self._button_bar.pack_start(self._status_label, True, True, 0) 149 150 if text != None: 151 self._status_label.set_markup(text) 152 elif self._status_label: 153 self._status_label.destroy() 154 155 if not self._button_bar and self.is_empty: 156 self.destroy() 157 158 def _attr_to_tag(self, attr): 159 buf = self.text_view.get_buffer() 160 table = buf.get_tag_table() 161 ret = [] 162 163 tp = attr.klass.type 164 165 if not tp in self._attr_map: 166 return None 167 168 propname, klass = self._attr_map[tp] 169 170 # The hack! Set __class__ so we can access .value/.color, 171 # then set __class__ back. This is expected to break at any time 172 cls = attr.__class__ 173 attr.__class__ = klass 174 175 if tp == Pango.AttrType.FOREGROUND or tp == Pango.AttrType.BACKGROUND: 176 value = attr.color 177 else: 178 value = attr.value 179 180 attr.__class__ = cls 181 182 tagname = str(tp) + ':' + str(value) 183 184 tag = table.lookup(tagname) 185 186 if not tag: 187 tag = buf.create_tag(tagname) 188 tag.set_property(propname, value) 189 190 return tag 191 192 def add_lines(self, line, use_markup=False): 193 buf = self.text_view.get_buffer() 194 195 if not buf.get_start_iter().equal(buf.get_end_iter()): 196 line = "\n" + line 197 198 if not use_markup: 199 buf.insert(buf.get_end_iter(), line) 200 return 201 202 try: 203 ret = Pango.parse_markup(line, -1, '\x00') 204 except Exception as e: 205 print('Could not parse markup:', e) 206 buf.insert(buf.get_end_iter(), line) 207 return 208 209 text = ret[2] 210 211 mark = buf.create_mark(None, buf.get_end_iter(), True) 212 buf.insert(buf.get_end_iter(), text) 213 214 attrs = [] 215 ret[1].filter(lambda x: attrs.append(x)) 216 217 for attr in attrs: 218 # Try/catch everything since the _attr_to_tag stuff is a big 219 # hack 220 try: 221 tag = self._attr_to_tag(attr) 222 except: 223 continue 224 225 if not tag is None: 226 start = buf.get_iter_at_mark(mark) 227 end = start.copy() 228 229 start.forward_chars(attr.start_index) 230 end.forward_chars(attr.end_index) 231 232 buf.apply_tag(tag, start, end) 233 234 def add_action(self, name, callback, data=None): 235 image = Gtk.Image.new_from_icon_name(name, Gtk.IconSize.MENU) 236 image.show() 237 238 self._ensure_button_bar() 239 240 ev = Gtk.EventBox() 241 ev.set_visible_window(True) 242 ev.add(image) 243 ev.show() 244 245 ev.set_halign(Gtk.Align.END) 246 247 self._button_bar.pack_end(ev, False, False, 0) 248 ev.get_window().set_cursor(Gdk.Cursor.new(Gdk.CursorType.HAND2)) 249 250 ev.connect('button-press-event', self._on_action_activate, callback, data) 251 ev.connect('enter-notify-event', self._on_action_enter_notify) 252 ev.connect('leave-notify-event', self._on_action_leave_notify) 253 254 ev.connect_after('destroy', self._on_action_destroy) 255 return ev 256 257 def clear(self): 258 self.text_view.get_buffer().set_text('') 259 260 def _ensure_button_bar(self): 261 if not self._button_bar: 262 self._button_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=3) 263 self._button_bar.show() 264 self.pack_start(self._button_bar, False, False, 0) 265 266 def _on_action_destroy(self, widget): 267 if self._button_bar and len(self._button_bar.get_children()) == 0: 268 self._button_bar.destroy() 269 self._button_bar = None 270 271 def _on_action_enter_notify(self, widget, evnt): 272 img = widget.get_child() 273 img.set_state(Gtk.StateType.PRELIGHT) 274 275 def _on_action_leave_notify(self, widget, evnt): 276 img = widget.get_child() 277 img.set_state(Gtk.StateType.NORMAL) 278 279 def _on_action_activate(self, widget, evnt, callback, data): 280 if data: 281 callback(data) 282 else: 283 callback() 284 285# ex:ts=4:et 286