foldertree.py
1 """ 2 Folder tree and messagelist widgets definitions. 3 """ 4 # pylint: disable=too-many-arguments,bad-super-call 5 # pylint: disable=attribute-defined-outside-init 6 7 from cgi import escape 8 9 from PyQt4 import QtCore, QtGui 10 11 from bmconfigparser import config 12 from helper_sql import sqlExecute, sqlQuery 13 from .settingsmixin import SettingsMixin 14 from tr import _translate 15 from .utils import avatarize 16 17 # for pylupdate 18 _translate("MainWindow", "inbox") 19 _translate("MainWindow", "new") 20 _translate("MainWindow", "sent") 21 _translate("MainWindow", "trash") 22 23 TimestampRole = QtCore.Qt.UserRole + 1 24 25 26 class AccountMixin(object): 27 """UI-related functionality for accounts""" 28 ALL = 0 29 NORMAL = 1 30 CHAN = 2 31 MAILINGLIST = 3 32 SUBSCRIPTION = 4 33 BROADCAST = 5 34 35 def accountColor(self): 36 """QT UI color for an account""" 37 if not self.isEnabled: 38 return QtGui.QColor(128, 128, 128) 39 elif self.type == self.CHAN: 40 return QtGui.QColor(216, 119, 0) 41 elif self.type in [self.MAILINGLIST, self.SUBSCRIPTION]: 42 return QtGui.QColor(137, 4, 177) 43 return QtGui.QApplication.palette().text().color() 44 45 def folderColor(self): 46 """QT UI color for a folder""" 47 if not self.parent().isEnabled: 48 return QtGui.QColor(128, 128, 128) 49 return QtGui.QApplication.palette().text().color() 50 51 def accountBrush(self): 52 """Account brush (for QT UI)""" 53 brush = QtGui.QBrush(self.accountColor()) 54 brush.setStyle(QtCore.Qt.NoBrush) 55 return brush 56 57 def folderBrush(self): 58 """Folder brush (for QT UI)""" 59 brush = QtGui.QBrush(self.folderColor()) 60 brush.setStyle(QtCore.Qt.NoBrush) 61 return brush 62 63 def accountString(self): 64 """Account string suitable for use in To: field: label <address>""" 65 label = self._getLabel() 66 return ( 67 self.address if label == self.address 68 else '%s <%s>' % (label, self.address) 69 ) 70 71 def setAddress(self, address): 72 """Set bitmessage address of the object""" 73 if address is None: 74 self.address = None 75 else: 76 self.address = str(address) 77 78 def setUnreadCount(self, cnt): 79 """Set number of unread messages""" 80 try: 81 if self.unreadCount == int(cnt): 82 return 83 except AttributeError: 84 pass 85 self.unreadCount = int(cnt) 86 if isinstance(self, QtGui.QTreeWidgetItem): 87 self.emitDataChanged() 88 89 def setEnabled(self, enabled): 90 """Set account enabled (QT UI)""" 91 self.isEnabled = enabled 92 try: 93 self.setExpanded(enabled) 94 except AttributeError: 95 pass 96 if isinstance(self, Ui_AddressWidget): 97 for i in range(self.childCount()): 98 if isinstance(self.child(i), Ui_FolderWidget): 99 self.child(i).setEnabled(enabled) 100 if isinstance(self, QtGui.QTreeWidgetItem): 101 self.emitDataChanged() 102 103 def setType(self): 104 """Set account type (QT UI)""" 105 self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable) 106 if self.address is None: 107 self.type = self.ALL 108 self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable) 109 elif config.safeGetBoolean(self.address, 'chan'): 110 self.type = self.CHAN 111 elif config.safeGetBoolean(self.address, 'mailinglist'): 112 self.type = self.MAILINGLIST 113 elif sqlQuery( 114 '''select label from subscriptions where address=?''', self.address): 115 self.type = AccountMixin.SUBSCRIPTION 116 else: 117 self.type = self.NORMAL 118 119 def defaultLabel(self): 120 """Default label (in case no label is set manually)""" 121 queryreturn = None 122 retval = None 123 if self.type in ( 124 AccountMixin.NORMAL, 125 AccountMixin.CHAN, AccountMixin.MAILINGLIST): 126 try: 127 retval = str( 128 config.get(self.address, 'label'), 'utf-8') 129 except Exception: 130 queryreturn = sqlQuery( 131 '''select label from addressbook where address=?''', self.address) 132 elif self.type == AccountMixin.SUBSCRIPTION: 133 queryreturn = sqlQuery( 134 '''select label from subscriptions where address=?''', self.address) 135 if queryreturn is not None: 136 if queryreturn != []: 137 for row in queryreturn: 138 retval, = row 139 retval = str(retval, 'utf-8') 140 elif self.address is None or self.type == AccountMixin.ALL: 141 return str( 142 str(_translate("MainWindow", "All accounts")), 'utf-8') 143 144 return retval or str(self.address, 'utf-8') 145 146 147 class BMTreeWidgetItem(QtGui.QTreeWidgetItem, AccountMixin): 148 """A common abstract class for Tree widget item""" 149 150 def __init__(self, parent, pos, address, unreadCount): 151 super(QtGui.QTreeWidgetItem, self).__init__() 152 self.setAddress(address) 153 self.setUnreadCount(unreadCount) 154 self._setup(parent, pos) 155 156 def _getAddressBracket(self, unreadCount=False): 157 return " (" + str(self.unreadCount) + ")" if unreadCount else "" 158 159 def data(self, column, role): 160 """Override internal QT method for returning object data""" 161 if column == 0: 162 if role == QtCore.Qt.DisplayRole: 163 return self._getLabel() + self._getAddressBracket( 164 self.unreadCount > 0) 165 elif role == QtCore.Qt.EditRole: 166 return self._getLabel() 167 elif role == QtCore.Qt.ToolTipRole: 168 return self._getLabel() + self._getAddressBracket(False) 169 elif role == QtCore.Qt.FontRole: 170 font = QtGui.QFont() 171 font.setBold(self.unreadCount > 0) 172 return font 173 return super(BMTreeWidgetItem, self).data(column, role) 174 175 176 class Ui_FolderWidget(BMTreeWidgetItem): 177 """Item in the account/folder tree representing a folder""" 178 folderWeight = {"inbox": 1, "new": 2, "sent": 3, "trash": 4} 179 180 def __init__( 181 self, parent, pos=0, address="", folderName="", unreadCount=0): 182 self.setFolderName(folderName) 183 super(Ui_FolderWidget, self).__init__( 184 parent, pos, address, unreadCount) 185 186 def _setup(self, parent, pos): 187 parent.insertChild(pos, self) 188 189 def _getLabel(self): 190 return _translate("MainWindow", self.folderName) 191 192 def setFolderName(self, fname): 193 """Set folder name (for QT UI)""" 194 self.folderName = str(fname) 195 196 def data(self, column, role): 197 """Override internal QT method for returning object data""" 198 if column == 0 and role == QtCore.Qt.ForegroundRole: 199 return self.folderBrush() 200 return super(Ui_FolderWidget, self).data(column, role) 201 202 # inbox, sent, thrash first, rest alphabetically 203 def __lt__(self, other): 204 if isinstance(other, Ui_FolderWidget): 205 if self.folderName in self.folderWeight: 206 x = self.folderWeight[self.folderName] 207 else: 208 x = 99 209 if other.folderName in self.folderWeight: 210 y = self.folderWeight[other.folderName] 211 else: 212 y = 99 213 reverse = QtCore.Qt.DescendingOrder == \ 214 self.treeWidget().header().sortIndicatorOrder() 215 if x == y: 216 return self.folderName < other.folderName 217 return x >= y if reverse else x < y 218 219 return super(QtGui.QTreeWidgetItem, self).__lt__(other) 220 221 222 class Ui_AddressWidget(BMTreeWidgetItem, SettingsMixin): 223 """Item in the account/folder tree representing an account""" 224 def __init__(self, parent, pos=0, address=None, unreadCount=0, enabled=True): 225 super(Ui_AddressWidget, self).__init__( 226 parent, pos, address, unreadCount) 227 self.setEnabled(enabled) 228 229 def _setup(self, parent, pos): 230 self.setType() 231 parent.insertTopLevelItem(pos, self) 232 233 def _getLabel(self): 234 if self.address is None: 235 return str(_translate( 236 "MainWindow", "All accounts").toUtf8(), 'utf-8', 'ignore') 237 else: 238 try: 239 return str( 240 config.get(self.address, 'label'), 241 'utf-8', 'ignore') 242 except: 243 return str(self.address, 'utf-8') 244 245 def _getAddressBracket(self, unreadCount=False): 246 ret = "" if self.isExpanded() \ 247 else super(Ui_AddressWidget, self)._getAddressBracket(unreadCount) 248 if self.address is not None: 249 ret += " (" + self.address + ")" 250 return ret 251 252 def data(self, column, role): 253 """Override internal QT method for returning object data""" 254 if column == 0: 255 if role == QtCore.Qt.DecorationRole: 256 return avatarize( 257 self.address or self._getLabel().encode('utf8')) 258 elif role == QtCore.Qt.ForegroundRole: 259 return self.accountBrush() 260 return super(Ui_AddressWidget, self).data(column, role) 261 262 def setData(self, column, role, value): 263 """Save account label (if you edit in the the UI, this will be triggered and will save it to keys.dat)""" 264 if role == QtCore.Qt.EditRole \ 265 and self.type != AccountMixin.SUBSCRIPTION: 266 config.set( 267 str(self.address), 'label', 268 str(value.toString().toUtf8()) 269 if isinstance(value, QtCore.QVariant) 270 else value.encode('utf-8') 271 ) 272 config.save() 273 return super(Ui_AddressWidget, self).setData(column, role, value) 274 275 def setAddress(self, address): 276 """Set address to object (for QT UI)""" 277 super(Ui_AddressWidget, self).setAddress(address) 278 self.setData(0, QtCore.Qt.UserRole, self.address) 279 280 def _getSortRank(self): 281 return self.type if self.isEnabled else (self.type + 100) 282 283 # label (or address) alphabetically, disabled at the end 284 def __lt__(self, other): 285 # pylint: disable=protected-access 286 if isinstance(other, Ui_AddressWidget): 287 reverse = QtCore.Qt.DescendingOrder == \ 288 self.treeWidget().header().sortIndicatorOrder() 289 if self._getSortRank() == other._getSortRank(): 290 x = self._getLabel().lower() 291 y = other._getLabel().lower() 292 return x < y 293 return ( 294 not reverse 295 if self._getSortRank() < other._getSortRank() else reverse 296 ) 297 298 return super(QtGui.QTreeWidgetItem, self).__lt__(other) 299 300 301 class Ui_SubscriptionWidget(Ui_AddressWidget): 302 """Special treating of subscription addresses""" 303 # pylint: disable=unused-argument 304 def __init__(self, parent, pos=0, address="", unreadCount=0, label="", enabled=True): 305 super(Ui_SubscriptionWidget, self).__init__( 306 parent, pos, address, unreadCount, enabled) 307 308 def _getLabel(self): 309 queryreturn = sqlQuery( 310 '''select label from subscriptions where address=?''', self.address) 311 if queryreturn != []: 312 for row in queryreturn: 313 retval, = row 314 return str(retval, 'utf-8', 'ignore') 315 return str(self.address, 'utf-8') 316 317 def setType(self): 318 """Set account type""" 319 super(Ui_SubscriptionWidget, self).setType() # sets it editable 320 self.type = AccountMixin.SUBSCRIPTION # overrides type 321 322 def setData(self, column, role, value): 323 """Save subscription label to database""" 324 if role == QtCore.Qt.EditRole: 325 if isinstance(value, QtCore.QVariant): 326 label = str( 327 value.toString().toUtf8()).decode('utf-8', 'ignore') 328 else: 329 label = str(value, 'utf-8', 'ignore') 330 sqlExecute( 331 '''UPDATE subscriptions SET label=? WHERE address=?''', 332 label, self.address) 333 return super(Ui_SubscriptionWidget, self).setData(column, role, value) 334 335 336 class BMTableWidgetItem(QtGui.QTableWidgetItem, SettingsMixin): 337 """A common abstract class for Table widget item""" 338 339 def __init__(self, label=None, unread=False): 340 super(QtGui.QTableWidgetItem, self).__init__() 341 self.setLabel(label) 342 self.setUnread(unread) 343 self._setup() 344 345 def _setup(self): 346 self.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) 347 348 def setLabel(self, label): 349 """Set object label""" 350 self.label = label 351 352 def setUnread(self, unread): 353 """Set/unset read state of an item""" 354 self.unread = unread 355 356 def data(self, role): 357 """Return object data (QT UI)""" 358 if role in ( 359 QtCore.Qt.DisplayRole, QtCore.Qt.EditRole, QtCore.Qt.ToolTipRole 360 ): 361 return self.label 362 elif role == QtCore.Qt.FontRole: 363 font = QtGui.QFont() 364 font.setBold(self.unread) 365 return font 366 return super(BMTableWidgetItem, self).data(role) 367 368 369 class BMAddressWidget(BMTableWidgetItem, AccountMixin): 370 """A common class for Table widget item with account""" 371 372 def _setup(self): 373 super(BMAddressWidget, self)._setup() 374 self.setEnabled(True) 375 self.setType() 376 377 def _getLabel(self): 378 return self.label 379 380 def data(self, role): 381 """Return object data (QT UI)""" 382 if role == QtCore.Qt.ToolTipRole: 383 return self.label + " (" + self.address + ")" 384 elif role == QtCore.Qt.DecorationRole: 385 if config.safeGetBoolean( 386 'bitmessagesettings', 'useidenticons'): 387 return avatarize(self.address or self.label) 388 elif role == QtCore.Qt.ForegroundRole: 389 return self.accountBrush() 390 return super(BMAddressWidget, self).data(role) 391 392 393 class MessageList_AddressWidget(BMAddressWidget): 394 """Address item in a messagelist""" 395 def __init__(self, address=None, label=None, unread=False): 396 self.setAddress(address) 397 super(MessageList_AddressWidget, self).__init__(label, unread) 398 399 def setLabel(self, label=None): 400 """Set label""" 401 super(MessageList_AddressWidget, self).setLabel(label) 402 if label is not None: 403 return 404 newLabel = self.address 405 queryreturn = None 406 if self.type in ( 407 AccountMixin.NORMAL, 408 AccountMixin.CHAN, AccountMixin.MAILINGLIST): 409 try: 410 newLabel = str( 411 config.get(self.address, 'label'), 412 'utf-8', 'ignore') 413 except: 414 queryreturn = sqlQuery( 415 '''select label from addressbook where address=?''', self.address) 416 elif self.type == AccountMixin.SUBSCRIPTION: 417 queryreturn = sqlQuery( 418 '''select label from subscriptions where address=?''', self.address) 419 if queryreturn: 420 for row in queryreturn: 421 newLabel = str(row[0], 'utf-8', 'ignore') 422 423 self.label = newLabel 424 425 def data(self, role): 426 """Return object data (QT UI)""" 427 if role == QtCore.Qt.UserRole: 428 return self.address 429 return super(MessageList_AddressWidget, self).data(role) 430 431 def setData(self, role, value): 432 """Set object data""" 433 if role == QtCore.Qt.EditRole: 434 self.setLabel() 435 return super(MessageList_AddressWidget, self).setData(role, value) 436 437 # label (or address) alphabetically, disabled at the end 438 def __lt__(self, other): 439 if isinstance(other, MessageList_AddressWidget): 440 return self.label.lower() < other.label.lower() 441 return super(QtGui.QTableWidgetItem, self).__lt__(other) 442 443 444 class MessageList_SubjectWidget(BMTableWidgetItem): 445 """Message list subject item""" 446 def __init__(self, subject=None, label=None, unread=False): 447 self.setSubject(subject) 448 super(MessageList_SubjectWidget, self).__init__(label, unread) 449 450 def setSubject(self, subject): 451 """Set subject""" 452 self.subject = subject 453 454 def data(self, role): 455 """Return object data (QT UI)""" 456 if role == QtCore.Qt.UserRole: 457 return self.subject 458 if role == QtCore.Qt.ToolTipRole: 459 return escape(str(self.subject, 'utf-8')) 460 return super(MessageList_SubjectWidget, self).data(role) 461 462 # label (or address) alphabetically, disabled at the end 463 def __lt__(self, other): 464 if isinstance(other, MessageList_SubjectWidget): 465 return self.label.lower() < other.label.lower() 466 return super(QtGui.QTableWidgetItem, self).__lt__(other) 467 468 469 # In order for the time columns on the Inbox and Sent tabs to be sorted 470 # correctly (rather than alphabetically), we need to overload the < 471 # operator and use this class instead of QTableWidgetItem. 472 class MessageList_TimeWidget(BMTableWidgetItem): 473 """ 474 A subclass of QTableWidgetItem for received (lastactiontime) field. 475 '<' operator is overloaded to sort by TimestampRole == 33 476 msgid is available by QtCore.Qt.UserRole 477 """ 478 479 def __init__(self, label=None, unread=False, timestamp=None, msgid=''): 480 super(MessageList_TimeWidget, self).__init__(label, unread) 481 self.setData(QtCore.Qt.UserRole, QtCore.QByteArray(msgid)) 482 self.setData(TimestampRole, int(timestamp)) 483 484 def __lt__(self, other): 485 return self.data(TimestampRole) < other.data(TimestampRole) 486 487 def data(self, role=QtCore.Qt.UserRole): 488 """ 489 Returns expected python types for QtCore.Qt.UserRole and TimestampRole 490 custom roles and super for any Qt role 491 """ 492 data = super(MessageList_TimeWidget, self).data(role) 493 if role == TimestampRole: 494 return int(data.toPyObject()) 495 if role == QtCore.Qt.UserRole: 496 return str(data.toPyObject()) 497 return data 498 499 500 class Ui_AddressBookWidgetItem(BMAddressWidget): 501 """Addressbook item""" 502 # pylint: disable=unused-argument 503 def __init__(self, label=None, acc_type=AccountMixin.NORMAL): 504 self.type = acc_type 505 super(Ui_AddressBookWidgetItem, self).__init__(label=label) 506 507 def data(self, role): 508 """Return object data""" 509 if role == QtCore.Qt.UserRole: 510 return self.type 511 return super(Ui_AddressBookWidgetItem, self).data(role) 512 513 def setData(self, role, value): 514 """Set data""" 515 if role == QtCore.Qt.EditRole: 516 self.label = str( 517 value.toString().toUtf8() 518 if isinstance(value, QtCore.QVariant) else value 519 ) 520 if self.type in ( 521 AccountMixin.NORMAL, 522 AccountMixin.MAILINGLIST, AccountMixin.CHAN): 523 try: 524 config.get(self.address, 'label') 525 config.set(self.address, 'label', self.label) 526 config.save() 527 except: 528 sqlExecute('''UPDATE addressbook set label=? WHERE address=?''', self.label, self.address) 529 elif self.type == AccountMixin.SUBSCRIPTION: 530 sqlExecute('''UPDATE subscriptions set label=? WHERE address=?''', self.label, self.address) 531 else: 532 pass 533 return super(Ui_AddressBookWidgetItem, self).setData(role, value) 534 535 def __lt__(self, other): 536 if isinstance(other, Ui_AddressBookWidgetItem): 537 reverse = QtCore.Qt.DescendingOrder == \ 538 self.tableWidget().horizontalHeader().sortIndicatorOrder() 539 540 if self.type == other.type: 541 return self.label.lower() < other.label.lower() 542 return not reverse if self.type < other.type else reverse 543 return super(QtGui.QTableWidgetItem, self).__lt__(other) 544 545 546 class Ui_AddressBookWidgetItemLabel(Ui_AddressBookWidgetItem): 547 """Addressbook label item""" 548 def __init__(self, address, label, acc_type): 549 self.address = address 550 super(Ui_AddressBookWidgetItemLabel, self).__init__(label, acc_type) 551 552 def data(self, role): 553 """Return object data""" 554 self.label = self.defaultLabel() 555 return super(Ui_AddressBookWidgetItemLabel, self).data(role) 556 557 558 class Ui_AddressBookWidgetItemAddress(Ui_AddressBookWidgetItem): 559 """Addressbook address item""" 560 def __init__(self, address, label, acc_type): 561 self.address = address 562 super(Ui_AddressBookWidgetItemAddress, self).__init__(address, acc_type) 563 564 def data(self, role): 565 """Return object data""" 566 if role == QtCore.Qt.ToolTipRole: 567 return self.address 568 if role == QtCore.Qt.DecorationRole: 569 return None 570 return super(Ui_AddressBookWidgetItemAddress, self).data(role) 571 572 573 class AddressBookCompleter(QtGui.QCompleter): 574 """Addressbook completer""" 575 576 def __init__(self): 577 super(AddressBookCompleter, self).__init__() 578 self.cursorPos = -1 579 580 def onCursorPositionChanged(self, oldPos, newPos): # pylint: disable=unused-argument 581 """Callback for cursor position change""" 582 if oldPos != self.cursorPos: 583 self.cursorPos = -1 584 585 def splitPath(self, path): 586 """Split on semicolon""" 587 text = str(path.toUtf8(), 'utf-8') 588 return [text[:self.widget().cursorPosition()].split(';')[-1].strip()] 589 590 def pathFromIndex(self, index): 591 """Perform autocompletion (reimplemented QCompleter method)""" 592 autoString = str( 593 index.data(QtCore.Qt.EditRole).toString().toUtf8(), 'utf-8') 594 text = str(self.widget().text().toUtf8(), 'utf-8') 595 596 # If cursor position was saved, restore it, else save it 597 if self.cursorPos != -1: 598 self.widget().setCursorPosition(self.cursorPos) 599 else: 600 self.cursorPos = self.widget().cursorPosition() 601 602 # Get current prosition 603 curIndex = self.widget().cursorPosition() 604 605 # prev_delimiter_index should actually point at final white space 606 # AFTER the delimiter 607 # Get index of last delimiter before current position 608 prevDelimiterIndex = text[0:curIndex].rfind(";") 609 while text[prevDelimiterIndex + 1] == " ": 610 prevDelimiterIndex += 1 611 612 # Get index of first delimiter after current position 613 # (or EOL if no delimiter after cursor) 614 nextDelimiterIndex = text.find(";", curIndex) 615 if nextDelimiterIndex == -1: 616 nextDelimiterIndex = len(text) 617 618 # Get part of string that occurs before cursor 619 part1 = text[0:prevDelimiterIndex + 1] 620 621 # Get string value from before auto finished string is selected 622 # pre = text[prevDelimiterIndex + 1:curIndex - 1] 623 624 # Get part of string that occurs AFTER cursor 625 part2 = text[nextDelimiterIndex:] 626 627 return part1 + autoString + part2