1import colorsys
2import sys
3import xml.etree.cElementTree as ET
4# from io import BytesIO
5
6from gi.repository import Gtk, Gdk, GObject, Pango
7from gi.repository.GdkPixbuf import Pixbuf
8
9from pychess.System import conf
10from pychess.System.Log import log
11from pychess.System.prefix import addDataPrefix
12
13
14def createCombo(combo, data=[], name=None, ellipsize_mode=None):
15    if name is not None:
16        combo.set_name(name)
17    lst_store = Gtk.ListStore(Pixbuf, str)
18    for row in data:
19        lst_store.append(row)
20    combo.clear()
21
22    combo.set_model(lst_store)
23    crp = Gtk.CellRendererPixbuf()
24    crp.set_property('xalign', 0)
25    crp.set_property('xpad', 2)
26    combo.pack_start(crp, False)
27    combo.add_attribute(crp, 'pixbuf', 0)
28
29    crt = Gtk.CellRendererText()
30    crt.set_property('xalign', 0)
31    crt.set_property('xpad', 4)
32    combo.pack_start(crt, True)
33    combo.add_attribute(crt, 'text', 1)
34    if ellipsize_mode is not None:
35        crt.set_property('ellipsize', ellipsize_mode)
36
37
38def updateCombo(combo, data):
39    def get_active(combobox):
40        model = combobox.get_model()
41        active = combobox.get_active()
42        if active < 0:
43            return None
44        return model[active][1]
45
46    last_active = get_active(combo)
47    lst_store = combo.get_model()
48    lst_store.clear()
49    new_active = 0
50    for i, row in enumerate(data):
51        lst_store.append(row)
52        if last_active == row[1]:
53            new_active = i
54    combo.set_active(new_active)
55
56
57def genColor(n, startpoint=0):
58    assert n >= 1
59    # This splits the 0 - 1 segment in the pizza way
60    hue = (2 * n - 1) / (2.**(n - 1).bit_length()) - 1
61    hue = (hue + startpoint) % 1
62    # We set saturation based on the amount of green, scaled to the interval
63    # [0.6..0.8]. This ensures a consistent lightness over all colors.
64    rgb = colorsys.hsv_to_rgb(hue, 1, 1)
65    rgb = colorsys.hsv_to_rgb(hue, 1, (1 - rgb[1]) * 0.2 + 0.6)
66    # This algorithm ought to balance colors more precisely, but it overrates
67    # the lightness of yellow, and nearly makes it black
68    # yiq = colorsys.rgb_to_yiq(*rgb)
69    # rgb = colorsys.yiq_to_rgb(.125, yiq[1], yiq[2])
70    return rgb
71
72
73def keepDown(scrolledWindow):
74    def changed(vadjust):
75        if not hasattr(vadjust, "need_scroll") or vadjust.need_scroll:
76            vadjust.set_value(vadjust.get_upper() - vadjust.get_page_size())
77            vadjust.need_scroll = True
78
79    scrolledWindow.get_vadjustment().connect("changed", changed)
80
81    def value_changed(vadjust):
82        vadjust.need_scroll = abs(vadjust.get_value() + vadjust.get_page_size() -
83                                  vadjust.get_upper()) < vadjust.get_step_increment()
84
85    scrolledWindow.get_vadjustment().connect("value-changed", value_changed)
86
87
88# wrap analysis text column. thanks to
89# http://www.islascruz.org/html/index.php?blog/show/Wrap-text-in-a-TreeView-column.html
90def appendAutowrapColumn(treeview, name, **kvargs):
91    cell = Gtk.CellRendererText()
92    # cell.props.wrap_mode = Pango.WrapMode.WORD
93    # TODO:
94    # changed to ellipsize instead until "never ending grow" bug gets fixed
95    # see https://github.com/pychess/pychess/issues/1054
96    cell.props.ellipsize = Pango.EllipsizeMode.END
97    column = Gtk.TreeViewColumn(name, cell, **kvargs)
98    treeview.append_column(column)
99
100    def callback(treeview, allocation, column, cell):
101        otherColumns = [c for c in treeview.get_columns() if c != column]
102        newWidth = allocation.width - sum(c.get_width() for c in otherColumns)
103
104        hsep = GObject.Value()
105        hsep.init(GObject.TYPE_INT)
106        hsep.set_int(0)
107        treeview.style_get_property("horizontal-separator", hsep)
108        newWidth -= hsep.get_int() * (len(otherColumns) + 1) * 2
109        if cell.props.wrap_width == newWidth or newWidth <= 0:
110            return
111        cell.props.wrap_width = newWidth
112        store = treeview.get_model()
113        store_iter = store.get_iter_first()
114        while store_iter and store.iter_is_valid(store_iter):
115            store.row_changed(store.get_path(store_iter), store_iter)
116            store_iter = store.iter_next(store_iter)
117        treeview.set_size_request(0, -1)
118    # treeview.connect_after("size-allocate", callback, column, cell)
119
120    scroll = treeview.get_parent()
121    if isinstance(scroll, Gtk.ScrolledWindow):
122        scroll.set_policy(Gtk.PolicyType.NEVER, scroll.get_policy()[1])
123
124    return cell
125
126
127METHODS = (
128    # Gtk.SpinButton should be listed prior to Gtk.Entry, as it is a
129    # subclass, but requires different handling
130    (Gtk.SpinButton, ("get_value", "set_value", "value-changed")),
131    (Gtk.Entry, ("get_text", "set_text", "changed")),
132    (Gtk.Expander, ("get_expanded", "set_expanded", "notify::expanded")),
133    (Gtk.ComboBox, ("get_active", "set_active", "changed")),
134    (Gtk.IconView, ("_get_active", "_set_active", "selection-changed")),
135    (Gtk.ToggleButton, ("get_active", "set_active", "toggled")),
136    (Gtk.CheckMenuItem, ("get_active", "set_active", "toggled")),
137    (Gtk.Range, ("get_value", "set_value", "value-changed")),
138    (Gtk.TreeSortable, ("get_value", "set_value", "sort-column-changed")),
139    (Gtk.Paned, ("get_position", "set_position", "notify::position")),
140)
141
142
143def keep(widget, key, get_value_=None, set_value_=None):  # , first_value=None):
144    if widget is None:
145        raise AttributeError("key '%s' isn't in widgets" % key)
146
147    for class_, methods_ in METHODS:
148        # Use try-except just to make spinx happy...
149        try:
150            if isinstance(widget, class_):
151                getter, setter, signal = methods_
152                break
153        except TypeError:
154            getter, setter, signal = methods_
155            break
156    else:
157        raise AttributeError("I don't have any knowledge of type: '%s'" %
158                             widget)
159
160    if get_value_:
161        def get_value():
162            return get_value_(widget)
163    else:
164        get_value = getattr(widget, getter)
165
166    if set_value_:
167        def set_value(v):
168            return set_value_(widget, v)
169    else:
170        set_value = getattr(widget, setter)
171
172    def setFromConf():
173        try:
174            v = conf.get(key)
175        except TypeError:
176            log.warning("uistuff.keep.setFromConf: Key '%s' from conf had the wrong type '%s', ignored" %
177                        (key, type(conf.get(key))))
178            # print("uistuff.keep TypeError %s %s" % (key, conf.get(key)))
179        else:
180            set_value(v)
181
182    def callback(*args):
183        if not conf.hasKey(key) or conf.get(key) != get_value():
184            conf.set(key, get_value())
185
186    widget.connect(signal, callback)
187    conf.notify_add(key, lambda *args: setFromConf())
188
189    if conf.hasKey(key):
190        setFromConf()
191    elif conf.get(key) is not None:
192        conf.set(key, conf.get(key))
193
194
195# loadDialogWidget() and saveDialogWidget() are similar to uistuff.keep() but are needed
196# for saving widget values for Gtk.Dialog instances that are loaded with different
197# sets of values/configurations and which also aren't instant save like in
198# uistuff.keep(), but rather are saved later if and when the user clicks
199# the dialog's OK button
200def loadDialogWidget(widget,
201                     widget_name,
202                     config_number,
203                     get_value_=None,
204                     set_value_=None,
205                     first_value=None):
206    key = widget_name + "-" + str(config_number)
207
208    if widget is None:
209        raise AttributeError("key '%s' isn't in widgets" % widget_name)
210
211    for class_, methods_ in METHODS:
212        if isinstance(widget, class_):
213            getter, setter, signal = methods_
214            break
215    else:
216        if set_value_ is None:
217            raise AttributeError("I don't have any knowledge of type: '%s'" %
218                                 widget)
219
220    if get_value_:
221        def get_value():
222            return get_value_(widget)
223    else:
224        get_value = getattr(widget, getter)
225
226    if set_value_:
227        def set_value(v):
228            return set_value_(widget, v)
229    else:
230        set_value = getattr(widget, setter)
231
232    if conf.hasKey(key):
233        try:
234            v = conf.get(key)
235        except TypeError:
236            log.warning("uistuff.loadDialogWidget: Key '%s' from conf had the wrong type '%s', ignored" %
237                        (key, type(conf.get(key))))
238            if first_value is not None:
239                conf.set(key, first_value)
240            else:
241                conf.set(key, get_value())
242        else:
243            set_value(v)
244    elif first_value is not None:
245        conf.set(key, first_value)
246        set_value(conf.get(key))
247    else:
248        log.warning("Didn't load widget \"%s\": no conf value and no first_value arg" % widget_name)
249
250
251def saveDialogWidget(widget, widget_name, config_number, get_value_=None):
252    key = widget_name + "-" + str(config_number)
253
254    if widget is None:
255        raise AttributeError("key '%s' isn't in widgets" % widget_name)
256
257    for class_, methods_ in METHODS:
258        if isinstance(widget, class_):
259            getter, setter, signal = methods_
260            break
261    else:
262        if get_value_ is None:
263            raise AttributeError("I don't have any knowledge of type: '%s'" %
264                                 widget)
265
266    if get_value_:
267        def get_value():
268            return get_value_(widget)
269    else:
270        get_value = getattr(widget, getter)
271
272    if not conf.hasKey(key) or conf.get(key) != get_value():
273        conf.set(key, get_value())
274
275
276POSITION_NONE, POSITION_CENTER, POSITION_GOLDEN = range(3)
277
278
279def keepWindowSize(key,
280                   window,
281                   defaultSize=None,
282                   defaultPosition=POSITION_NONE):
283    """ You should call keepWindowSize before show on your windows """
284
285    key = key + "window"
286
287    def savePosition(window, *event):
288        log.debug("keepWindowSize.savePosition: %s" % window.get_title())
289        width = window.get_allocation().width
290        height = window.get_allocation().height
291        x_loc, y_loc = window.get_position()
292
293        if width <= 0:
294            log.error("Setting width = '%d' for %s to conf" % (width, key))
295        if height <= 0:
296            log.error("Setting height = '%d' for %s to conf" % (height, key))
297
298        log.debug("Saving window position width=%s height=%s x=%s y=%s" %
299                  (width, height, x_loc, y_loc))
300        conf.set(key + "_width", width)
301        conf.set(key + "_height", height)
302        conf.set(key + "_x", x_loc)
303        conf.set(key + "_y", y_loc)
304
305        return False
306
307    window.connect("delete-event", savePosition, "delete-event")
308
309    def loadPosition(window):
310        # log.debug("keepWindowSize.loadPosition: %s" % window.title)
311        # Just to make sphinx happy...
312        try:
313            width, height = window.get_size_request()
314        except TypeError:
315            pass
316
317        if conf.hasKey(key + "_width") and conf.hasKey(key + "_height"):
318            width = conf.get(key + "_width")
319            height = conf.get(key + "_height")
320            log.debug("Resizing window to width=%s height=%s" %
321                      (width, height))
322            window.resize(width, height)
323
324        elif defaultSize:
325            width, height = defaultSize
326            log.debug("Resizing window to width=%s height=%s" %
327                      (width, height))
328            window.resize(width, height)
329
330        elif key == "mainwindow":
331            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
332            width = int(monitor_width / 2)
333            height = int(monitor_height / 4) * 3
334            log.debug("Resizing window to width=%s height=%s" %
335                      (width, height))
336            window.resize(width, height)
337
338        elif key == "preferencesdialogwindow":
339            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
340            width = int(monitor_width / 2)
341            height = int(monitor_height / 4) * 3
342            window.resize(1, 1)
343        else:
344            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
345            width = int(monitor_width / 2)
346            height = int(monitor_height / 4) * 3
347
348        if conf.hasKey(key + "_x") and conf.hasKey(key + "_y"):
349            x = max(0, conf.get(key + "_x"))
350            y = max(0, conf.get(key + "_y"))
351            log.debug("Moving window to x=%s y=%s" % (x, y))
352            window.move(x, y)
353
354        elif defaultPosition in (POSITION_CENTER, POSITION_GOLDEN):
355            monitor_x, monitor_y, monitor_width, monitor_height = getMonitorBounds()
356            x_loc = int(monitor_width / 2 - width / 2) + monitor_x
357            if defaultPosition == POSITION_CENTER:
358                y_loc = int(monitor_height / 2 - height / 2) + monitor_y
359            else:
360                # Place the window on the upper golden ratio line
361                y_loc = int(monitor_height / 2.618 - height / 2) + monitor_y
362            log.debug("Moving window to x=%s y=%s" % (x_loc, y_loc))
363            window.move(x_loc, y_loc)
364
365    loadPosition(window)
366
367    # In rare cases, gtk throws some gtk_size_allocation error, which is
368    # probably a race condition. To avoid the window forgets its size in
369    # these cases, we add this extra hook
370    def callback(window):
371        loadPosition(window)
372
373    onceWhenReady(window, callback)
374
375
376# Some properties can only be set, once the window is sufficiently initialized,
377# This function lets you queue your request until that has happened.
378def onceWhenReady(window, func, *args, **kwargs):
379    def cb(window, alloc, func, *args, **kwargs):
380        func(window, *args, **kwargs)
381        window.disconnect(handler_id)
382
383    handler_id = window.connect_after("size-allocate", cb, func, *args, **
384                                      kwargs)
385
386
387def getMonitorBounds():
388    screen = Gdk.Screen.get_default()
389    root_window = screen.get_root_window()
390    # Just to make sphinx happy...
391    try:
392        ptr_window, mouse_x, mouse_y, mouse_mods = root_window.get_pointer()
393        current_monitor_number = screen.get_monitor_at_point(mouse_x, mouse_y)
394        monitor_geometry = screen.get_monitor_geometry(current_monitor_number)
395        return monitor_geometry.x, monitor_geometry.y, monitor_geometry.width, monitor_geometry.height
396    except TypeError:
397        return (0, 0, 0, 0)
398
399
400def makeYellow(box):
401    def on_box_expose_event(box, context):
402        # box.style.paint_flat_box (box.window,
403        #    Gtk.StateType.NORMAL, Gtk.ShadowType.NONE, None, box, "tooltip",
404        #    box.allocation.x, box.allocation.y,
405        #    box.allocation.width, box.allocation.height)
406        pass
407
408    def cb(box):
409        tooltip = Gtk.Window(Gtk.WindowType.POPUP)
410        tooltip.set_name('gtk-tooltip')
411        tooltip.ensure_style()
412        tooltipStyle = tooltip.get_style()
413        box.set_style(tooltipStyle)
414        box.connect("draw", on_box_expose_event)
415
416    onceWhenReady(box, cb)
417
418
419class GladeWidgets:
420    """ A simple class that wraps a the glade get_widget function
421        into the python __getitem__ version """
422
423    def __init__(self, filename):
424        # TODO: remove this when upstream fixes translations with Python3+Windows
425        if sys.platform == "win32" and not conf.no_gettext:
426            tree = ET.parse(addDataPrefix("glade/%s" % filename))
427            for node in tree.iter():
428                if 'translatable' in node.attrib:
429                    node.text = _(node.text)
430                    del node.attrib['translatable']
431                if node.get('name') in ('pixbuf', 'logo'):
432                    node.text = addDataPrefix("glade/%s" % node.text)
433            xml_text = ET.tostring(tree.getroot(), encoding='unicode', method='xml')
434            self.builder = Gtk.Builder.new_from_string(xml_text, -1)
435        else:
436            self.builder = Gtk.Builder()
437            if not conf.no_gettext:
438                self.builder.set_translation_domain("pychess")
439            self.builder.add_from_file(addDataPrefix("glade/%s" % filename))
440
441    def __getitem__(self, key):
442        return self.builder.get_object(key)
443
444    def getGlade(self):
445        return self.builder
446