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