1from gi.repository import Gtk, GObject, Gdk, Pango 2 3from pychess.Utils.IconLoader import load_icon 4from pychess.widgets.InfoPanel import Panel 5 6TYPE_PERSONAL, TYPE_CHANNEL, TYPE_GUEST, \ 7 TYPE_ADMIN, TYPE_COMP, TYPE_BLINDFOLD = range(6) 8 9add_icon = load_icon(16, "gtk-add", "list-add") 10remove_icon = load_icon(16, "gtk-remove", "list-remove") 11 12 13def cmp(x, y): 14 return (x > y) - (x < y) 15 16 17class TextImageTree(Gtk.TreeView): 18 """ :Description: Defines a tree with two columns. 19 The first one has text. The second one a clickable stock_icon 20 """ 21 22 __gsignals__ = { 23 'activated': (GObject.SignalFlags.RUN_FIRST, None, (str, str, int)), 24 'selected': (GObject.SignalFlags.RUN_FIRST, None, (str, int)) 25 } 26 27 def __init__(self, icon): 28 GObject.GObject.__init__(self) 29 self.id2iter = {} 30 31 pm = Gtk.ListStore(str, str, int, str) 32 self.sort_model = Gtk.TreeModelSort(model=pm) 33 self.set_model(self.sort_model) 34 self.idSet = set() 35 36 self.set_headers_visible(False) 37 self.set_tooltip_column(3) 38 self.set_search_column(1) 39 self.sort_model.set_sort_column_id(1, Gtk.SortType.ASCENDING) 40 self.sort_model.set_sort_func(1, self.compareFunction, 1) 41 42 # First column 43 crp = Gtk.CellRendererPixbuf() 44 crp.props.pixbuf = icon 45 self.rightcol = Gtk.TreeViewColumn("", crp) 46 self.append_column(self.rightcol) 47 48 # Second column 49 crt = Gtk.CellRendererText() 50 crt.props.ellipsize = Pango.EllipsizeMode.END 51 self.leftcol = Gtk.TreeViewColumn("", crt, text=1) 52 self.leftcol.set_expand(True) 53 self.append_column(self.leftcol) 54 55 # Mouse 56 self.pressed = None 57 self.stdcursor = Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR) 58 self.linkcursor = Gdk.Cursor.new(Gdk.CursorType.HAND2) 59 self.connect("button_press_event", self.button_press) 60 self.connect("button_release_event", self.button_release) 61 self.connect("motion_notify_event", self.motion_notify) 62 self.connect("leave_notify_event", self.leave_notify) 63 64 # Selection 65 self.get_selection().connect("changed", self.selection_changed) 66 67 def addRow(self, grp_id, text, grp_type): 68 """ :Description: Takes a player or a channel identified by grp_id and adds 69 them to the correct group defined by grp_type 70 :return: None 71 """ 72 if grp_id in self.id2iter: 73 return 74 model = self.sort_model.get_model() 75 m_iter = model.append([grp_id, text, grp_type, GObject.markup_escape_text(text)]) 76 self.id2iter[grp_id] = m_iter 77 self.idSet.add(grp_id) 78 79 def removeRow(self, grp_id): 80 """ :Description: Takes a player or channel identified by grp_id and removes them from 81 the data model. 82 :return: None 83 """ 84 try: 85 m_iter = self.id2iter[grp_id] 86 except KeyError: 87 return 88 model = self.sort_model.get_model() 89 model.remove(m_iter) 90 del self.id2iter[grp_id] 91 self.idSet.remove(grp_id) 92 93 def selectRow(self, grp_id): 94 """ :Description: Takes a grp_id and finds the row associated with this id then 95 sets this row to be the focus ie selected 96 97 :returns: None 98 """ 99 m_iter = self.id2iter[grp_id] 100 m_iter = self.sort_model.convert_child_iter_to_iter(m_iter)[1] 101 sel = self.get_selection() 102 sel.select_iter(m_iter) 103 104 def __contains__(self, grp_id): 105 """ :Description: Checks to see if a grp_id in a member of the id set 106 :returns: boolean 107 """ 108 return grp_id in self.idSet 109 110 def button_press(self, widget, event): 111 path_col_pos = self.get_path_at_pos(int(event.x), int(event.y)) 112 if path_col_pos and path_col_pos[1] == self.rightcol: 113 self.pressed = path_col_pos[0] 114 115 def button_release(self, widget, event): 116 path_col_pos = self.get_path_at_pos(int(event.x), int(event.y)) 117 if path_col_pos and path_col_pos[1] == self.rightcol: 118 if self.pressed == path_col_pos[0]: 119 model = self.sort_model 120 m_iter = model.get_iter(self.pressed) 121 grp_id = model.get_value(m_iter, 0) 122 text = model.get_value(m_iter, 1) 123 grp_type = model.get_value(m_iter, 2) 124 self.emit("activated", grp_id, text, grp_type) 125 self.pressed = None 126 127 def motion_notify(self, widget, event): 128 path_col_pos = self.get_path_at_pos(int(event.x), int(event.y)) 129 if path_col_pos and path_col_pos[1] == self.rightcol: 130 self.get_window().set_cursor(self.linkcursor) 131 else: 132 self.get_window().set_cursor(self.stdcursor) 133 134 def leave_notify(self, widget, event): 135 self.get_window().set_cursor(self.stdcursor) 136 137 def selection_changed(self, selection): 138 model, m_iter = selection.get_selected() 139 if m_iter: 140 grp_id = model.get_value(m_iter, 0) 141 grp_type = model.get_value(m_iter, 2) 142 self.emit("selected", grp_id, grp_type) 143 144 def compareFunction(self, treemodel, iter0, iter1, column): 145 val0 = treemodel.get_value(iter0, column).split(":")[0] 146 val1 = treemodel.get_value(iter1, column).split(":")[0] 147 if val0.isdigit() and val1.isdigit(): 148 return cmp(int(val0), int(val1)) 149 return cmp(val0, val1) 150 151 152class ChannelsPanel(Gtk.ScrolledWindow, Panel): 153 154 __gsignals__ = { 155 'conversationAdded': (GObject.SignalFlags.RUN_FIRST, None, 156 (str, str, int)), 157 'conversationRemoved': (GObject.SignalFlags.RUN_FIRST, None, (str, )), 158 'conversationSelected': (GObject.SignalFlags.RUN_FIRST, None, (str, )) 159 } 160 161 def __init__(self, connection): 162 GObject.GObject.__init__(self) 163 self.connection = connection 164 165 self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 166 vbox = Gtk.VBox() 167 self.add_with_viewport(vbox) 168 self.get_child().set_shadow_type(Gtk.ShadowType.NONE) 169 170 self.joinedList = TextImageTree(remove_icon) 171 self.joinedList.connect("activated", self.onRemove) 172 self.joinedList.connect("selected", self.onSelect) 173 vbox.pack_start(self.joinedList, True, True, 0) 174 175 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 176 expander = Gtk.Expander.new(_("Friends")) 177 vbox.pack_start(expander, False, True, 0) 178 self.friendsList = TextImageTree(add_icon) 179 self.friendsList.connect("activated", self.onAdd) 180 self.friendsList.fixed_height_mode = True 181 connection.cm.connect("privateMessage", self.onPersonMessage) 182 connection.cm.connect("channelsListed", self.onChannelsListed) 183 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 184 expander.add(self.friendsList) 185 self.channels = {} 186 187 expander = Gtk.Expander.new(_("Admin")) 188 vbox.pack_start(expander, False, True, 0) 189 self.adminList = TextImageTree(add_icon) 190 self.adminList.connect("activated", self.onAdd) 191 self.adminList.fixed_height_mode = True 192 connection.cm.connect("privateMessage", self.onPersonMessage) 193 connection.cm.connect("channelsListed", self.onChannelsListed) 194 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 195 expander.add(self.adminList) 196 197 expander = Gtk.Expander.new(_("More channels")) 198 vbox.pack_start(expander, False, True, 0) 199 self.channelsList = TextImageTree(add_icon) 200 self.channelsList.connect("activated", self.onAdd) 201 self.channelsList.fixed_height_mode = True 202 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 203 expander.add(self.channelsList) 204 205 expander = Gtk.Expander.new(_("More players")) 206 vbox.pack_start(expander, False, True, 0) 207 self.playersList = TextImageTree(add_icon) 208 self.playersList.connect("activated", self.onAdd) 209 self.playersList.fixed_height_mode = True 210 connection.cm.connect("privateMessage", self.onPersonMessage) 211 connection.cm.connect("channelsListed", self.onChannelsListed) 212 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 213 expander.add(self.playersList) 214 215 expander = Gtk.Expander.new(_("Computers")) 216 vbox.pack_start(expander, False, True, 0) 217 self.compList = TextImageTree(add_icon) 218 self.compList.connect("activated", self.onAdd) 219 self.compList.fixed_height_mode = True 220 connection.cm.connect("privateMessage", self.onPersonMessage) 221 connection.cm.connect("channelsListed", self.onChannelsListed) 222 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 223 expander.add(self.compList) 224 225 expander = Gtk.Expander.new(_("BlindFold")) 226 vbox.pack_start(expander, False, True, 0) 227 self.blindList = TextImageTree(add_icon) 228 self.blindList.connect("activated", self.onAdd) 229 self.blindList.fixed_height_mode = True 230 connection.cm.connect("privateMessage", self.onPersonMessage) 231 connection.cm.connect("channelsListed", self.onChannelsListed) 232 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 233 expander.add(self.blindList) 234 235 expander = Gtk.Expander.new(_("Guests")) 236 vbox.pack_start(expander, False, True, 0) 237 self.guestList = TextImageTree(add_icon) 238 self.guestList.connect("activated", self.onAdd) 239 self.guestList.fixed_height_mode = True 240 connection.cm.connect("privateMessage", self.onPersonMessage) 241 connection.cm.connect("channelsListed", self.onChannelsListed) 242 vbox.pack_start(Gtk.Separator.new(0), False, False, 2) 243 expander.add(self.guestList) 244 245 self.channels = {} 246 self.highlighted = {} 247 248 def change_fg_colour(self, lc, cell, model, m_iter, data): 249 """ 250 :Description: Changes the foreground colour of a cell 251 252 :param lc: :class:`Gtk.TreeViewColumn` The column we are interested in 253 :param cell: :class:`Gtk.CellRenderer` The cell we want to change 254 :param model: :class:`Gtk.TreeModel` 255 :param iter: :class:`Gtk.TreeIter` 256 :param data: :py:class:`dict` (key=int,value=bool) value is true if channel already highlighted 257 :return: None 258 """ 259 260 for chan in data: 261 if model[m_iter][0] == chan: 262 if data[chan]: 263 cell.set_property('foreground_rgba', 264 Gdk.RGBA(0.9, 0.2, 0.2, 1)) 265 else: 266 cell.set_property('foreground_rgba', Gdk.RGBA(0, 0, 0, 1)) 267 268 def channel_Highlight(self, a, channel, grp_type, b): 269 """ 270 :Description: Highlights a channel ( that is **not** in focus ) that has received an update and 271 changes it's foreground colour to represent change in contents 272 273 :param a: not used 274 :param channel: **(str)** The channel the message is intended for 275 :param grp_type: either TYPE_CHANNEL or TYPE_PERSONAL 276 :param b: not used 277 :return: None 278 """ 279 j_list = self.joinedList 280 leftcol = j_list.leftcol # treeViewColumn 281 282 cur_iter = j_list.get_selection().get_selected()[1] # Selected iter 283 if grp_type == TYPE_PERSONAL: 284 channel = "person" + channel.lower() 285 tmp_iter = j_list.id2iter[channel] 286 tmp_iter = j_list.sort_model.convert_child_iter_to_iter(tmp_iter)[1] # channel iter 287 j_list.get_selection().select_iter(tmp_iter) 288 cell = leftcol.get_cells()[0] 289 j_list.get_selection().select_iter(cur_iter) 290 self.highlighted[channel] = True 291 if cur_iter != tmp_iter: 292 # iter = tmp_iter 293 leftcol.set_cell_data_func(cell, 294 self.change_fg_colour, 295 func_data=self.highlighted) 296 297 def start(self): 298 self.channels = self.connection.cm.getChannels() 299 if self.channels: 300 self._addChannels(self.channels) 301 for player in list(self.connection.players.values()): 302 grp_id = self.compileId(player.name, TYPE_PERSONAL) 303 if str(player.name) in self.connection.notify_users: 304 self.friendsList.addRow( 305 grp_id, player.name + player.display_titles(), TYPE_PERSONAL) 306 elif player.online and ('(B)' in player.display_titles()): 307 self.blindList.addRow( 308 grp_id, player.name + player.display_titles(), TYPE_BLINDFOLD) 309 elif player.online and ('(C)' in player.display_titles()): 310 self.compList.addRow(grp_id, player.name + player.display_titles(), 311 TYPE_COMP) 312 elif player.online and ('Guest' in str(player.name)): 313 self.guestList.addRow( 314 grp_id, player.name + player.display_titles(), TYPE_GUEST) 315 elif player.online: 316 self.playersList.addRow( 317 grp_id, player.name + player.display_titles(), TYPE_PERSONAL) 318 319 def addPlayer(players, new_players): 320 for player in new_players: 321 # print("Player : %s : %s" % (str(player.name),player.display_titles())) 322 if str(player.name) in self.connection.notify_users: 323 self.friendsList.addRow( 324 self.compileId(player.name, TYPE_PERSONAL), 325 player.name + player.display_titles(), TYPE_PERSONAL) 326 elif '(C)' in str(player.display_titles()): 327 self.compList.addRow( 328 self.compileId(player.name, TYPE_COMP), 329 player.name + player.display_titles(), TYPE_COMP) 330 elif '(B)' in str(player.display_titles()): 331 self.blindList.addRow( 332 self.compileId(player.name, TYPE_BLINDFOLD), 333 player.name + player.display_titles(), TYPE_BLINDFOLD) 334 elif 'Guest' in str(player.name): 335 self.guestList.addRow( 336 self.compileId(player.name, TYPE_GUEST), 337 player.name + player.display_titles(), TYPE_GUEST) 338 else: 339 self.playersList.addRow( 340 self.compileId(player.name, TYPE_PERSONAL), 341 player.name + player.display_titles(), TYPE_PERSONAL) 342 return False 343 344 self.connection.players.connect("FICSPlayerEntered", addPlayer) 345 346 def removePlayer(players, player): 347 if (str(player.name) in list(self.connection.notify_users)): 348 self.friendsList.removeRow(self.compileId(player.name, 349 TYPE_PERSONAL)) 350 else: 351 self.playersList.removeRow(self.compileId(player.name, 352 TYPE_PERSONAL)) 353 return False 354 355 self.connection.players.connect("FICSPlayerExited", removePlayer) 356 357 def _addChannels(self, channels): 358 for grp_id, name in channels: 359 grp_id = self.compileId(grp_id, TYPE_CHANNEL) 360 self.channelsList.addRow(grp_id, str(grp_id) + ": " + name, TYPE_CHANNEL) 361 362 for grp_id, name in channels: 363 if grp_id in self.connection.cm.getJoinedChannels(): 364 grp_id = self.compileId(grp_id, TYPE_CHANNEL) 365 if grp_id.isdigit(): 366 self.onAdd(self.channelsList, grp_id, str(grp_id) + ": " + name, 367 TYPE_CHANNEL) 368 else: 369 self.onAdd(self.channelsList, grp_id, name, TYPE_CHANNEL) 370 371 def onChannelsListed(self, cm, channels): 372 if not self.channels: 373 self.channels = channels 374 self._addChannels(channels) 375 376 def compileId(self, grp_id, type): 377 if type == TYPE_CHANNEL: 378 # FIXME: We can't really add stuff to the grp_id, as panels use it to 379 # identify the channel 380 assert not grp_id.startswith("person"), "Oops, this is a problem" 381 else: 382 grp_id = "person" + grp_id.lower() 383 return grp_id 384 385 def onAdd(self, grp_list, grp_id, text, grp_type): 386 if grp_id in grp_list: 387 grp_list.removeRow(grp_id) 388 self.joinedList.addRow(grp_id, text, grp_type) 389 self.emit('conversationAdded', grp_id, text, grp_type) 390 if grp_type == TYPE_CHANNEL: 391 self.connection.cm.joinChannel(grp_id) 392 self.joinedList.selectRow(grp_id) 393 394 def onRemove(self, joined_list, grp_id, text, grp_type): 395 joined_list.removeRow(grp_id) 396 if grp_type == TYPE_CHANNEL: 397 self.channelsList.addRow(grp_id, text, grp_type) 398 elif grp_type == TYPE_PERSONAL: 399 self.playersList.addRow(grp_id, text, grp_type) 400 elif grp_type == TYPE_COMP: 401 self.compList.addRow(grp_id, text, grp_type) 402 elif grp_type == TYPE_ADMIN: 403 self.adminList.addRow(grp_id, text, grp_type) 404 elif grp_type == TYPE_GUEST: 405 self.guestList.addRow(grp_id, text, grp_type) 406 elif grp_type == TYPE_BLINDFOLD: 407 self.blindList.addRow(grp_id, text, grp_type) 408 409 self.emit('conversationRemoved', grp_id) 410 if grp_type == TYPE_CHANNEL: 411 self.connection.cm.removeChannel(grp_id) 412 413 def onSelect(self, joined_list, grp_id, grp_type): 414 self.emit('conversationSelected', grp_id) 415 joined_list.get_selection().get_selected()[1] # Selected iter 416 cell = joined_list.leftcol.get_cells()[0] 417 self.highlighted[grp_id] = False 418 joined_list.leftcol.set_cell_data_func(cell, 419 self.change_fg_colour, 420 func_data=self.highlighted) 421 422 def onPersonMessage(self, cm, name, title, isadmin, text): 423 if not self.compileId(name, TYPE_PERSONAL) in self.joinedList: 424 grp_id = self.compileId(name, TYPE_PERSONAL) 425 self.onAdd(self.playersList, grp_id, name, TYPE_PERSONAL) 426