Address Tree updates

The Address tree now is sorted and updates when number of unread
messages changes.
This commit is contained in:
mailchuck 2015-10-02 22:24:46 +02:00 committed by Peter Surda
parent 83a069d1f6
commit 83109796fe
3 changed files with 328 additions and 188 deletions

View File

@ -6,6 +6,21 @@ try:
except ImportError: except ImportError:
MessagingMenu = None MessagingMenu = None
try:
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import *
from PyQt4.QtGui import *
except Exception as err:
print 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download it from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\' (without quotes).'
print 'Error message:', err
sys.exit()
try:
_encoding = QtGui.QApplication.UnicodeUTF8
except AttributeError:
print 'QtGui.QApplication.UnicodeUTF8 error:', err
from addresses import * from addresses import *
import shared import shared
from bitmessageui import * from bitmessageui import *
@ -13,6 +28,7 @@ from namecoin import namecoinConnection, ensureNamecoinOptions
from newaddressdialog import * from newaddressdialog import *
from newaddresswizard import * from newaddresswizard import *
from migrationwizard import * from migrationwizard import *
from foldertree import *
from addaddressdialog import * from addaddressdialog import *
from newsubscriptiondialog import * from newsubscriptiondialog import *
from regenerateaddresses import * from regenerateaddresses import *
@ -38,125 +54,12 @@ import subprocess
import datetime import datetime
from helper_sql import * from helper_sql import *
import l10n import l10n
from utils import *
try: from collections import OrderedDict
from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import *
from PyQt4.QtGui import *
except Exception as err:
print 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download it from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\' (without quotes).'
print 'Error message:', err
sys.exit()
try:
_encoding = QtGui.QApplication.UnicodeUTF8
except AttributeError:
print 'QtGui.QApplication.UnicodeUTF8 error:', err
def _translate(context, text): def _translate(context, text):
return QtGui.QApplication.translate(context, text) return QtGui.QApplication.translate(context, text)
def identiconize(address):
size = 48
# If you include another identicon library, please generate an
# example identicon with the following md5 hash:
# 3fd4bf901b9d4ea1394f0fb358725b28
try:
identicon_lib = shared.config.get('bitmessagesettings', 'identiconlib')
except:
# default to qidenticon_two_x
identicon_lib = 'qidenticon_two_x'
# As an 'identiconsuffix' you could put "@bitmessge.ch" or "@bm.addr" to make it compatible with other identicon generators. (Note however, that E-Mail programs might convert the BM-address to lowercase first.)
# It can be used as a pseudo-password to salt the generation of the identicons to decrease the risk
# of attacks where someone creates an address to mimic someone else's identicon.
identiconsuffix = shared.config.get('bitmessagesettings', 'identiconsuffix')
if not shared.config.getboolean('bitmessagesettings', 'useidenticons'):
idcon = QtGui.QIcon()
return idcon
if (identicon_lib[:len('qidenticon')] == 'qidenticon'):
# print identicon_lib
# originally by:
# :Author:Shin Adachi <shn@glucose.jp>
# Licesensed under FreeBSD License.
# stripped from PIL and uses QT instead (by sendiulo, same license)
import qidenticon
hash = hashlib.md5(addBMIfNotPresent(address)+identiconsuffix).hexdigest()
use_two_colors = (identicon_lib[:len('qidenticon_two')] == 'qidenticon_two')
opacity = int(not((identicon_lib == 'qidenticon_x') | (identicon_lib == 'qidenticon_two_x') | (identicon_lib == 'qidenticon_b') | (identicon_lib == 'qidenticon_two_b')))*255
penwidth = 0
image = qidenticon.render_identicon(int(hash, 16), size, use_two_colors, opacity, penwidth)
# filename = './images/identicons/'+hash+'.png'
# image.save(filename)
idcon = QtGui.QIcon()
idcon.addPixmap(image, QtGui.QIcon.Normal, QtGui.QIcon.Off)
return idcon
elif identicon_lib == 'pydenticon':
# print identicon_lib
# Here you could load pydenticon.py (just put it in the "src" folder of your Bitmessage source)
from pydenticon import Pydenticon
# It is not included in the source, because it is licensed under GPLv3
# GPLv3 is a copyleft license that would influence our licensing
# Find the source here: http://boottunes.googlecode.com/svn-history/r302/trunk/src/pydenticon.py
# note that it requires PIL to be installed: http://www.pythonware.com/products/pil/
idcon_render = Pydenticon(addBMIfNotPresent(address)+identiconsuffix, size*3)
rendering = idcon_render._render()
data = rendering.convert("RGBA").tostring("raw", "RGBA")
qim = QImage(data, size, size, QImage.Format_ARGB32)
pix = QPixmap.fromImage(qim)
idcon = QtGui.QIcon()
idcon.addPixmap(pix, QtGui.QIcon.Normal, QtGui.QIcon.Off)
return idcon
def avatarize(address):
"""
loads a supported image for the given address' hash form 'avatars' folder
falls back to default avatar if 'default.*' file exists
falls back to identiconize(address)
"""
idcon = QtGui.QIcon()
hash = hashlib.md5(addBMIfNotPresent(address)).hexdigest()
str_broadcast_subscribers = '[Broadcast subscribers]'
if address == str_broadcast_subscribers:
# don't hash [Broadcast subscribers]
hash = address
# http://pyqt.sourceforge.net/Docs/PyQt4/qimagereader.html#supportedImageFormats
# print QImageReader.supportedImageFormats ()
# QImageReader.supportedImageFormats ()
extensions = ['PNG', 'GIF', 'JPG', 'JPEG', 'SVG', 'BMP', 'MNG', 'PBM', 'PGM', 'PPM', 'TIFF', 'XBM', 'XPM', 'TGA']
# try to find a specific avatar
for ext in extensions:
lower_hash = shared.appdata + 'avatars/' + hash + '.' + ext.lower()
upper_hash = shared.appdata + 'avatars/' + hash + '.' + ext.upper()
if os.path.isfile(lower_hash):
# print 'found avatar of ', address
idcon.addFile(lower_hash)
return idcon
elif os.path.isfile(upper_hash):
# print 'found avatar of ', address
idcon.addFile(upper_hash)
return idcon
# if we haven't found any, try to find a default avatar
for ext in extensions:
lower_default = shared.appdata + 'avatars/' + 'default.' + ext.lower()
upper_default = shared.appdata + 'avatars/' + 'default.' + ext.upper()
if os.path.isfile(lower_default):
default = lower_default
idcon.addFile(lower_default)
return idcon
elif os.path.isfile(upper_default):
default = upper_default
idcon.addFile(upper_default)
return idcon
# If no avatar is found
return identiconize(address)
def change_translation(locale): def change_translation(locale):
global qtranslator global qtranslator
qtranslator = QtCore.QTranslator() qtranslator = QtCore.QTranslator()
@ -168,29 +71,6 @@ def change_translation(locale):
qtranslator.load(translationpath) qtranslator.load(translationpath)
QtGui.QApplication.installTranslator(qtranslator) QtGui.QApplication.installTranslator(qtranslator)
def address_compare(x, y):
if x == "bitmessagesettings":
return -1
elif y == "bitmessagesettings":
return 1
if shared.config.getboolean(x, 'enabled') == shared.config.getboolean(y, 'enabled'):
if shared.config.get(x, 'label'):
x1 = shared.config.get(x, 'label').decode('utf-8').lower()
else:
x1 = x.decode('utf-8').lower()
if shared.config.get(y, 'label'):
y1 = shared.config.get(y, 'label').decode('utf-8').lower()
else:
y1 = y.decode('utf-8').lower()
if x1 > y1:
return 1
elif x1 < y1:
return -1
else:
return 0
else:
return (-1 if shared.config.getboolean(x, 'enabled') else 1)
class MyForm(QtGui.QMainWindow): class MyForm(QtGui.QMainWindow):
# sound type constants # sound type constants
@ -566,25 +446,21 @@ class MyForm(QtGui.QMainWindow):
elif tab == 'chan': elif tab == 'chan':
treeWidget = self.ui.treeWidgetChans treeWidget = self.ui.treeWidgetChans
treeWidget.clear() #treeWidget.clear()
# get number of (unread) messages # init dictionary
cntUnreadMsg = {} db = {}
queryreturn = sqlQuery('SELECT toaddress, folder, count(msgid) as cnt FROM inbox WHERE read = 0 GROUP BY toaddress, folder') enabled = {}
for row in queryreturn:
toaddress, folder, cnt = row for toAddress in shared.config.sections():
cntUnreadMsg[toaddress + folder] = cnt if toAddress == 'bitmessagesettings':
configSections = sorted(shared.config.sections(), cmp=address_compare)
for addressInKeysFile in configSections:
if addressInKeysFile == 'bitmessagesettings':
continue continue
isEnabled = shared.config.getboolean( isEnabled = shared.config.getboolean(
addressInKeysFile, 'enabled') toAddress, 'enabled')
isChan = shared.safeConfigGetBoolean( isChan = shared.safeConfigGetBoolean(
addressInKeysFile, 'chan') toAddress, 'chan')
isMaillinglist = shared.safeConfigGetBoolean( isMaillinglist = shared.safeConfigGetBoolean(
addressInKeysFile, 'mailinglist') toAddress, 'mailinglist')
if tab == 'messages': if tab == 'messages':
if isChan: if isChan:
@ -593,39 +469,65 @@ class MyForm(QtGui.QMainWindow):
if not isChan: if not isChan:
continue continue
newItem = QtGui.QTreeWidgetItem(treeWidget) db[toAddress] = {}
newItem.setExpanded(True)
newItem.setIcon(0, avatarize(addressInKeysFile))
newItem.setText(0, unicode(
shared.config.get(addressInKeysFile, 'label'), 'utf-8)')
+ ' (' + addressInKeysFile + ')')
newItem.setData(0, Qt.UserRole, [str(addressInKeysFile), "inbox"])
#set text color
if isEnabled:
if isMaillinglist:
brush = QtGui.QBrush(QtGui.QColor(137, 04, 177))
else:
brush = QtGui.QBrush(QApplication.palette().text().color())
else:
brush = QtGui.QBrush(QtGui.QColor(128, 128, 128))
newItem.setExpanded(False)
brush.setStyle(QtCore.Qt.NoBrush)
newItem.setForeground(0, brush)
for folder in folders: for folder in folders:
newSubItem = QtGui.QTreeWidgetItem(newItem) db[toAddress][folder] = 0
enabled[toAddress] = isEnabled
cnt = cntUnreadMsg.get(addressInKeysFile + folder, False) # get number of (unread) messages
if cnt: queryreturn = sqlQuery('SELECT toaddress, folder, count(msgid) as cnt FROM inbox WHERE read = 0 GROUP BY toaddress, folder')
unreadText = " (" + str(cnt) + ")" for row in queryreturn:
font = QtGui.QFont() toaddress, folder, cnt = row
font.setBold(True) if toaddress in db and folder in db[toaddress]:
newSubItem.setFont(0, font) db[toaddress][folder] = cnt
else:
unreadText = "" if treeWidget.isSortingEnabled():
treeWidget.setSortingEnabled(False)
widgets = {}
for i in range (0, treeWidget.topLevelItemCount()):
widget = treeWidget.topLevelItem(i)
toAddress = widget.address
if not toAddress in db:
treeWidget.takeTopLevelItem(i)
i -= 1
continue
unread = 0
for j in range (0, widget.childCount()):
subwidget = widget.child(j)
try:
subwidget.setUnreadCount(db[toAddress][subwidget.folderName])
unread += db[toAddress][subwidget.folderName]
db[toAddress].pop(subwidget.folderName, None)
except:
widget.takeChild(i)
j -= 1
newSubItem.setText(0, _translate("MainWindow", folder) + unreadText) # add missing folders
newSubItem.setData(0, Qt.UserRole, [str(addressInKeysFile), folder]) if len(db[toAddress]) > 0:
i = 0
for f, c in db[toAddress].iteritems():
print "adding %s, %i" % (f, c)
subwidget = Ui_FolderWidget(widget, i, toAddress, f, c)
i += 1
widget.setUnreadCount(unread)
db.pop(toAddress, None)
i = 0
for toAddress in db:
widget = Ui_AddressWidget(treeWidget, i, toAddress, db[toAddress]["inbox"])
j = 0
unread = 0
for folder in folders:
subwidget = Ui_FolderWidget(widget, j, toAddress, folder, db[toAddress][folder])
unread += db[toAddress][folder]
j += 1
widget.setUnreadCount(unread)
i += 1
treeWidget.setSortingEnabled(True)
def __init__(self, parent=None): def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent) QtGui.QWidget.__init__(self, parent)
@ -1851,6 +1753,12 @@ class MyForm(QtGui.QMainWindow):
def changedInboxUnread(self): def changedInboxUnread(self):
self.drawTrayIcon(self.currentTrayIconFileName, self.findInboxUnreadCount()) self.drawTrayIcon(self.currentTrayIconFileName, self.findInboxUnreadCount())
if self.ui.tabWidget.currentIndex() == 0:
self.rerenderTabTreeMessages()
elif self.ui.tabWidget.currentIndex() == 2:
self.rerenderTabTreeSubscriptions()
elif self.ui.tabWidget.currentIndex() == 3:
self.rerenderTabTreeChans()
def findInboxUnreadCount(self): def findInboxUnreadCount(self):
queryreturn = sqlQuery('''SELECT count(*) from inbox WHERE folder='inbox' and read=0''') queryreturn = sqlQuery('''SELECT count(*) from inbox WHERE folder='inbox' and read=0''')
@ -3615,12 +3523,7 @@ more work your computer must do to send the message. A Time-To-Live of four or f
tableWidget.item(currentRow, 2).setFont(font) tableWidget.item(currentRow, 2).setFont(font)
tableWidget.item(currentRow, 3).setFont(font) tableWidget.item(currentRow, 3).setFont(font)
self.changedInboxUnread() self.changedInboxUnread()
# if self.ui.tabWidget.currentIndex() == 0:
# self.rerenderTabTreeMessages()
# elif self.ui.tabWidget.currentIndex() == 2:
# self.rerenderTabTreeSubscriptions()
# elif self.ui.tabWidget.currentIndex() == 3:
# self.rerenderTabTreeChans()
else: else:
data = self.getCurrentMessageId() data = self.getCurrentMessageId()
if data != False: if data != False:

View File

@ -0,0 +1,133 @@
from PyQt4 import QtCore, QtGui
from utils import *
import shared
class Ui_FolderWidget(QtGui.QTreeWidgetItem):
folderWeight = {"inbox": 1, "sent": 2, "trash": 3}
def __init__(self, parent, pos = 0, address = "", folderName = "", unreadCount = 0):
super(QtGui.QTreeWidgetItem, self).__init__()
self.address = address
self.folderName = folderName
self.unreadCount = unreadCount
parent.insertChild(pos, self)
self.updateText()
def setAddress(self, address):
self.address = str(address)
self.updateText()
def setUnreadCount(self, cnt):
self.unreadCount = int(cnt)
self.updateText()
def setFolderName(self, fname):
self.folderName = str(fname)
self.updateText()
def updateText(self):
text = QtGui.QApplication.translate("MainWindow", self.folderName)
font = QtGui.QFont()
if self.unreadCount > 0:
text += " (" + str(self.unreadCount) + ")"
font.setBold(True)
else:
font.setBold(False)
self.setFont(0, font)
self.setText(0, text)
self.setToolTip(0, text)
self.setData(0, QtCore.Qt.UserRole, [self.address, self.folderName])
# inbox, sent, thrash first, rest alphabetically
def __lt__(self, other):
if (isinstance(other, Ui_FolderWidget)):
if self.folderName in self.folderWeight:
x = self.folderWeight[self.folderName]
else:
x = 4
if other.folderName in self.folderWeight:
y = self.folderWeight[other.folderName]
else:
y = 4
if x == y:
return self.folderName > other.folderName
else:
return x > y
return super(QtGui.QTreeWidgetItem, self).__lt__(other)
class Ui_AddressWidget(QtGui.QTreeWidgetItem):
def __init__(self, parent, pos = 0, address = "", unreadCount = 0):
super(QtGui.QTreeWidgetItem, self).__init__()
self.address = address
self.unreadCount = unreadCount
parent.insertTopLevelItem(pos, self)
# only set default when creating
#super(QtGui.QTreeWidgetItem, self).setExpanded(shared.config.getboolean(self.address, 'enabled'))
self.setExpanded(shared.safeConfigGetBoolean(self.address, 'enabled'))
self.updateText()
def setAddress(self, address):
self.address = str(address)
self.updateText()
def setUnreadCount(self, cnt):
self.unreadCount = int(cnt)
self.updateText()
def updateText(self):
text = QtGui.QApplication.translate("MainWindow",
unicode(shared.config.get(self.address, 'label'), 'utf-8)')
+ ' (' + self.address + ')')
font = QtGui.QFont()
if self.unreadCount > 0:
# only show message count if the child doesn't show
if not self.isExpanded():
text += " (" + str(self.unreadCount) + ")"
font.setBold(True)
else:
font.setBold(False)
self.setFont(0, font)
#set text color
if shared.safeConfigGetBoolean(self.address, 'enabled'):
if shared.safeConfigGetBoolean(self.address, 'mailinglist'):
brush = QtGui.QBrush(QtGui.QColor(137, 04, 177))
else:
brush = QtGui.QBrush(QtGui.QApplication.palette().text().color())
#self.setExpanded(True)
else:
brush = QtGui.QBrush(QtGui.QColor(128, 128, 128))
#self.setExpanded(False)
brush.setStyle(QtCore.Qt.NoBrush)
self.setForeground(0, brush)
self.setIcon(0, avatarize(self.address))
self.setText(0, text)
self.setToolTip(0, text)
self.setData(0, QtCore.Qt.UserRole, [self.address, "inbox"])
def setExpanded(self, expand):
super(Ui_AddressWidget, self).setExpanded(expand)
self.updateText()
# label (or address) alphabetically, disabled at the end
def __lt__(self, other):
if (isinstance(other, Ui_AddressWidget)):
if shared.config.getboolean(self.address, 'enabled') == shared.config.getboolean(other.address, 'enabled'):
if shared.config.get(self.address, 'label'):
x = shared.config.get(self.address, 'label').decode('utf-8').lower()
else:
x = self.address.decode('utf-8').lower()
if shared.config.get(other.address, 'label'):
y = shared.config.get(other.address, 'label').decode('utf-8').lower()
else:
y = other.address.decode('utf-8').lower()
return y < x
# else:
return (False if shared.config.getboolean(self.address, 'enabled') else True)
return super(QtGui.QTreeWidgetItem, self).__lt__(other)

104
src/bitmessageqt/utils.py Normal file
View File

@ -0,0 +1,104 @@
from PyQt4 import QtGui
import hashlib
import os
import shared
from addresses import addBMIfNotPresent
def identiconize(address):
size = 48
# If you include another identicon library, please generate an
# example identicon with the following md5 hash:
# 3fd4bf901b9d4ea1394f0fb358725b28
try:
identicon_lib = shared.config.get('bitmessagesettings', 'identiconlib')
except:
# default to qidenticon_two_x
identicon_lib = 'qidenticon_two_x'
# As an 'identiconsuffix' you could put "@bitmessge.ch" or "@bm.addr" to make it compatible with other identicon generators. (Note however, that E-Mail programs might convert the BM-address to lowercase first.)
# It can be used as a pseudo-password to salt the generation of the identicons to decrease the risk
# of attacks where someone creates an address to mimic someone else's identicon.
identiconsuffix = shared.config.get('bitmessagesettings', 'identiconsuffix')
if not shared.config.getboolean('bitmessagesettings', 'useidenticons'):
idcon = QtGui.QIcon()
return idcon
if (identicon_lib[:len('qidenticon')] == 'qidenticon'):
# print identicon_lib
# originally by:
# :Author:Shin Adachi <shn@glucose.jp>
# Licesensed under FreeBSD License.
# stripped from PIL and uses QT instead (by sendiulo, same license)
import qidenticon
hash = hashlib.md5(addBMIfNotPresent(address)+identiconsuffix).hexdigest()
use_two_colors = (identicon_lib[:len('qidenticon_two')] == 'qidenticon_two')
opacity = int(not((identicon_lib == 'qidenticon_x') | (identicon_lib == 'qidenticon_two_x') | (identicon_lib == 'qidenticon_b') | (identicon_lib == 'qidenticon_two_b')))*255
penwidth = 0
image = qidenticon.render_identicon(int(hash, 16), size, use_two_colors, opacity, penwidth)
# filename = './images/identicons/'+hash+'.png'
# image.save(filename)
idcon = QtGui.QIcon()
idcon.addPixmap(image, QtGui.QIcon.Normal, QtGui.QIcon.Off)
return idcon
elif identicon_lib == 'pydenticon':
# print identicon_lib
# Here you could load pydenticon.py (just put it in the "src" folder of your Bitmessage source)
from pydenticon import Pydenticon
# It is not included in the source, because it is licensed under GPLv3
# GPLv3 is a copyleft license that would influence our licensing
# Find the source here: http://boottunes.googlecode.com/svn-history/r302/trunk/src/pydenticon.py
# note that it requires PIL to be installed: http://www.pythonware.com/products/pil/
idcon_render = Pydenticon(addBMIfNotPresent(address)+identiconsuffix, size*3)
rendering = idcon_render._render()
data = rendering.convert("RGBA").tostring("raw", "RGBA")
qim = QImage(data, size, size, QImage.Format_ARGB32)
pix = QPixmap.fromImage(qim)
idcon = QtGui.QIcon()
idcon.addPixmap(pix, QtGui.QIcon.Normal, QtGui.QIcon.Off)
return idcon
def avatarize(address):
"""
loads a supported image for the given address' hash form 'avatars' folder
falls back to default avatar if 'default.*' file exists
falls back to identiconize(address)
"""
idcon = QtGui.QIcon()
hash = hashlib.md5(addBMIfNotPresent(address)).hexdigest()
str_broadcast_subscribers = '[Broadcast subscribers]'
if address == str_broadcast_subscribers:
# don't hash [Broadcast subscribers]
hash = address
# http://pyqt.sourceforge.net/Docs/PyQt4/qimagereader.html#supportedImageFormats
# print QImageReader.supportedImageFormats ()
# QImageReader.supportedImageFormats ()
extensions = ['PNG', 'GIF', 'JPG', 'JPEG', 'SVG', 'BMP', 'MNG', 'PBM', 'PGM', 'PPM', 'TIFF', 'XBM', 'XPM', 'TGA']
# try to find a specific avatar
for ext in extensions:
lower_hash = shared.appdata + 'avatars/' + hash + '.' + ext.lower()
upper_hash = shared.appdata + 'avatars/' + hash + '.' + ext.upper()
if os.path.isfile(lower_hash):
# print 'found avatar of ', address
idcon.addFile(lower_hash)
return idcon
elif os.path.isfile(upper_hash):
# print 'found avatar of ', address
idcon.addFile(upper_hash)
return idcon
# if we haven't found any, try to find a default avatar
for ext in extensions:
lower_default = shared.appdata + 'avatars/' + 'default.' + ext.lower()
upper_default = shared.appdata + 'avatars/' + 'default.' + ext.upper()
if os.path.isfile(lower_default):
default = lower_default
idcon.addFile(lower_default)
return idcon
elif os.path.isfile(upper_default):
default = upper_default
idcon.addFile(upper_default)
return idcon
# If no avatar is found
return identiconize(address)