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