Merge branch 'file_transfers'

Conflicts:
	src/mainscreen.py
This commit is contained in:
ingvar1995 2016-03-22 20:32:29 +03:00
commit 4c1a02c9a3
15 changed files with 520 additions and 43 deletions

View file

@ -4,6 +4,7 @@ from settings import Settings
from profile import Profile from profile import Profile
from toxcore_enums_and_consts import * from toxcore_enums_and_consts import *
from tox import bin_to_string from tox import bin_to_string
from ctypes import c_char_p, cast, pointer
class InvokeEvent(QtCore.QEvent): class InvokeEvent(QtCore.QEvent):
@ -28,6 +29,10 @@ _invoker = Invoker()
def invoke_in_main_thread(fn, *args, **kwargs): def invoke_in_main_thread(fn, *args, **kwargs):
QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs)) QtCore.QCoreApplication.postEvent(_invoker, InvokeEvent(fn, *args, **kwargs))
# -----------------------------------------------------------------------------------------------------------------
# Callbacks - current user
# -----------------------------------------------------------------------------------------------------------------
def self_connection_status(tox_link): def self_connection_status(tox_link):
""" """
@ -45,6 +50,11 @@ def self_connection_status(tox_link):
return wrapped return wrapped
# -----------------------------------------------------------------------------------------------------------------
# Callbacks - friends
# -----------------------------------------------------------------------------------------------------------------
def friend_status(tox, friend_num, new_status, user_data): def friend_status(tox, friend_num, new_status, user_data):
""" """
Check friend's status (none, busy, away) Check friend's status (none, busy, away)
@ -64,6 +74,8 @@ def friend_connection_status(tox, friend_num, new_status, user_data):
friend = profile.get_friend_by_number(friend_num) friend = profile.get_friend_by_number(friend_num)
if new_status == TOX_CONNECTION['NONE']: if new_status == TOX_CONNECTION['NONE']:
invoke_in_main_thread(friend.set_status, None) invoke_in_main_thread(friend.set_status, None)
elif friend.status is None:
invoke_in_main_thread(profile.send_avatar, friend_num)
invoke_in_main_thread(profile.update_filtration) invoke_in_main_thread(profile.update_filtration)
@ -116,6 +128,62 @@ def friend_request(tox, public_key, message, message_size, user_data):
tox_id = bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE) tox_id = bin_to_string(public_key, TOX_PUBLIC_KEY_SIZE)
invoke_in_main_thread(profile.process_friend_request, tox_id, message.decode('utf-8')) invoke_in_main_thread(profile.process_friend_request, tox_id, message.decode('utf-8'))
# -----------------------------------------------------------------------------------------------------------------
# Callbacks - file transfers
# -----------------------------------------------------------------------------------------------------------------
def tox_file_recv(window, tray):
def wrapped(tox, friend_number, file_number, file_type, size, file_name, file_name_size, user_data):
profile = Profile.get_instance()
settings = Settings.get_instance()
if file_type == TOX_FILE_KIND['DATA']:
print 'file'
file_name = file_name[:file_name_size]
invoke_in_main_thread(profile.incoming_file_transfer,
friend_number,
file_number,
size,
file_name)
if not window.isActiveWindow():
friend = profile.get_friend_by_number(friend_number)
if settings['notifications']:
invoke_in_main_thread(tray_notification, 'File from ' + friend.name, file_name, tray)
if settings['sound_notifications']:
sound_notification(SOUND_NOTIFICATION['FILE_TRANSFER'])
else: # AVATAR
print 'Avatar'
invoke_in_main_thread(profile.incoming_avatar,
friend_number,
file_number,
size)
return wrapped
def file_recv_chunk(tox, friend_number, file_number, position, chunk, length, user_data):
invoke_in_main_thread(Profile.get_instance().incoming_chunk,
friend_number,
file_number,
position,
chunk[:length] if length else None)
def file_chunk_request(tox, friend_number, file_number, position, size, user_data):
Profile.get_instance().outgoing_chunk(
friend_number,
file_number,
position,
size)
def file_recv_control(tox, friend_number, file_number, file_control, user_data):
# TODO: process
pass
# -----------------------------------------------------------------------------------------------------------------
# Callbacks - initialization
# -----------------------------------------------------------------------------------------------------------------
def init_callbacks(tox, window, tray): def init_callbacks(tox, window, tray):
""" """
@ -124,10 +192,16 @@ def init_callbacks(tox, window, tray):
:param window: main window :param window: main window
:param tray: tray (for notifications) :param tray: tray (for notifications)
""" """
tox.callback_self_connection_status(self_connection_status(tox), 0)
tox.callback_friend_status(friend_status, 0) tox.callback_friend_status(friend_status, 0)
tox.callback_friend_message(friend_message(window, tray), 0) tox.callback_friend_message(friend_message(window, tray), 0)
tox.callback_self_connection_status(self_connection_status(tox), 0)
tox.callback_friend_connection_status(friend_connection_status, 0) tox.callback_friend_connection_status(friend_connection_status, 0)
tox.callback_friend_name(friend_name, 0) tox.callback_friend_name(friend_name, 0)
tox.callback_friend_status_message(friend_status_message, 0) tox.callback_friend_status_message(friend_status_message, 0)
tox.callback_friend_request(friend_request, 0) tox.callback_friend_request(friend_request, 0)
tox.callback_file_recv(tox_file_recv(window, tray), 0)
tox.callback_file_recv_chunk(file_recv_chunk, 0)
tox.callback_file_chunk_request(file_chunk_request, 0)
tox.callback_file_recv_control(file_recv_control, 0)

154
src/file_transfers.py Normal file
View file

@ -0,0 +1,154 @@
from toxcore_enums_and_consts import TOX_FILE_KIND, TOX_FILE_CONTROL
from os.path import basename, getsize, exists
from os import remove
from time import time
from tox import Tox
import profile
from PySide import QtCore
TOX_FILE_TRANSFER_STATE = {
'RUNNING': 0,
'PAUSED': 1,
'CANCELED': 2,
'FINISHED': 3,
}
class StateSignal(QtCore.QObject):
signal = QtCore.Signal(int, float)
class FileTransfer(QtCore.QObject):
def __init__(self, path, tox, friend_number, size, file_number=None):
QtCore.QObject.__init__(self)
self._path = path
self._tox = tox
self._friend_number = friend_number
self.state = TOX_FILE_TRANSFER_STATE['RUNNING']
self._file_number = file_number
self._creation_time = time()
self._size = float(size)
self._done = 0
self._state_changed = StateSignal()
def set_tox(self, tox):
self._tox = tox
def set_state_changed_handler(self, handler):
self._state_changed.signal.connect(handler)
def get_file_number(self):
return self._file_number
def get_friend_number(self):
return self._friend_number
def cancel(self):
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self._file.close()
self._state_changed.signal.emit(self.state, self._done / self._size)
def send_control(self, control):
if self._tox.file_control(self._friend_number, self._file_number, control):
self.state = control
self._state_changed.signal.emit(self.state, self._done / self._size if self._size else 0)
def get_file_id(self):
return self._tox.file_get_file_id(self._friend_number, self._file_number)
def file_seek(self):
# TODO implement or not implement
pass
class SendTransfer(FileTransfer):
def __init__(self, path, tox, friend_number, kind=TOX_FILE_KIND['DATA'], file_id=None):
if path is not None:
self._file = open(path, 'rb')
size = getsize(path)
else:
size = 0
super(SendTransfer, self).__init__(path, tox, friend_number, size)
self._file_number = tox.file_send(friend_number, kind, size, file_id,
basename(path).encode('utf-8') if path else '')
def send_chunk(self, position, size):
if size:
self._file.seek(position)
data = self._file.read(size)
self._tox.file_send_chunk(self._friend_number, self._file_number, position, data)
self._done += size
self._state_changed.signal.emit(self.state, self._done / self._size)
else:
self._file.close()
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
self._state_changed.signal.emit(self.state, 1)
class SendAvatar(SendTransfer):
def __init__(self, path, tox, friend_number):
if path is None:
hash = None
else:
with open(path, 'rb') as fl:
hash = Tox.hash(fl.read())
super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], hash)
class ReceiveTransfer(FileTransfer):
def __init__(self, path, tox, friend_number, size, file_number):
super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number)
self._file = open(self._path, 'wb')
self._file.truncate(0)
self._file_size = 0
def cancel(self):
super(ReceiveTransfer, self).cancel()
remove(self._path)
def write_chunk(self, position, data):
if data is None:
self._file.close()
self.state = TOX_FILE_TRANSFER_STATE['FINISHED']
self._state_changed.signal.emit(self.state, self._done / self._size if self._size else 0)
else:
data = ''.join(chr(x) for x in data)
if self._file_size < position:
self._file.seek(0, 2)
self._file.write('\0' * (position - self._file_size))
self._file.seek(position)
self._file.write(data)
self._file.flush()
l = len(data)
if position + l > self._file_size:
self._file_size = position + l
self._done += l
self._state_changed.signal.emit(self.state, self._done / self._size)
class ReceiveAvatar(ReceiveTransfer):
def __init__(self, tox, friend_number, size, file_number):
path = profile.ProfileHelper.get_path() + '/avatars/{}.png'.format(tox.friend_get_public_key(friend_number))
super(ReceiveAvatar, self).__init__(path, tox, friend_number, size, file_number)
if exists(path):
if not size:
remove(path)
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self.state = TOX_FILE_TRANSFER_STATE['CANCELED']
else:
hash = self.get_file_id()
with open(path, 'rb') as fl:
existing_hash = Tox.hash(fl.read())
if hash == existing_hash:
self.send_control(TOX_FILE_CONTROL['CANCEL'])
self.state = TOX_FILE_TRANSFER_STATE['CANCELED']
else:
self.send_control(TOX_FILE_CONTROL['RESUME'])
else:
self.send_control(TOX_FILE_CONTROL['RESUME'])

BIN
src/images/accept.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

BIN
src/images/decline.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,5 +1,8 @@
from toxcore_enums_and_consts import * from toxcore_enums_and_consts import *
from PySide import QtGui, QtCore from PySide import QtGui, QtCore
import profile
from file_transfers import TOX_FILE_TRANSFER_STATE
from util import curr_directory
class MessageEdit(QtGui.QPlainTextEdit): class MessageEdit(QtGui.QPlainTextEdit):
@ -154,3 +157,97 @@ class StatusCircle(QtGui.QWidget):
paint.setPen(color) paint.setPen(color)
paint.drawEllipse(center, rad_x + 3, rad_y + 3) paint.drawEllipse(center, rad_x + 3, rad_y + 3)
paint.end() paint.end()
class FileTransferItem(QtGui.QListWidget):
def __init__(self, file_name, size, time, user, friend_number, file_number, show_accept, parent=None):
QtGui.QListWidget.__init__(self, parent)
self.resize(QtCore.QSize(600, 50))
self.setStyleSheet('QListWidget { background-color: green; }')
self.name = QtGui.QLabel(self)
self.name.setGeometry(QtCore.QRect(0, 15, 95, 20))
self.name.setTextFormat(QtCore.Qt.PlainText)
font = QtGui.QFont()
font.setFamily("Times New Roman")
font.setPointSize(11)
font.setBold(True)
self.name.setFont(font)
self.name.setObjectName("name")
self.name.setText(user if len(user) <= 14 else user[:11] + '...')
self.name.setStyleSheet('QLabel { color: black; }')
self.time = QtGui.QLabel(self)
self.time.setGeometry(QtCore.QRect(550, 0, 50, 50))
font.setPointSize(10)
font.setBold(False)
self.time.setFont(font)
self.time.setObjectName("time")
self.time.setText(time)
self.time.setStyleSheet('QLabel { color: black; }')
self.cancel = QtGui.QPushButton(self)
self.cancel.setGeometry(QtCore.QRect(500, 0, 50, 50))
pixmap = QtGui.QPixmap(curr_directory() + '/images/decline.png')
icon = QtGui.QIcon(pixmap)
self.cancel.setIcon(icon)
self.cancel.setIconSize(QtCore.QSize(50, 50))
self.cancel.clicked.connect(lambda: self.cancel_transfer(friend_number, file_number))
self.accept = QtGui.QPushButton(self)
self.accept.setGeometry(QtCore.QRect(450, 0, 50, 50))
pixmap = QtGui.QPixmap(curr_directory() + '/images/accept.png')
icon = QtGui.QIcon(pixmap)
self.accept.setIcon(icon)
self.accept.setIconSize(QtCore.QSize(50, 50))
self.accept.clicked.connect(lambda: self.accept_transfer(friend_number, file_number, size))
self.accept.setVisible(show_accept)
self.pb = QtGui.QProgressBar(self)
self.pb.setGeometry(QtCore.QRect(100, 15, 100, 20))
self.pb.setValue(0)
self.file_name = QtGui.QLabel(self)
self.file_name.setGeometry(QtCore.QRect(210, 0, 230, 50))
font.setPointSize(12)
self.file_name.setFont(font)
self.file_name.setObjectName("time")
file_size = size / 1024
if not file_size:
file_size = '<1KB'
elif file_size >= 1024:
file_size = '{}MB'.format(file_size / 1024)
else:
file_size = '{}KB'.format(file_size)
file_data = u'{} {}'.format(file_size, file_name)
self.file_name.setText(file_data if len(file_data) <= 27 else file_data[:24] + '...')
self.file_name.setStyleSheet('QLabel { color: black; }')
self.saved_name = file_name
def cancel_transfer(self, friend_number, file_number):
pr = profile.Profile.get_instance()
pr.cancel_transfer(friend_number, file_number)
self.setStyleSheet('QListWidget { background-color: red; }')
self.cancel.setVisible(False)
self.accept.setVisible(False)
self.pb.setVisible(False)
def accept_transfer(self, friend_number, file_number, size):
directory = QtGui.QFileDialog.getExistingDirectory()
if directory:
pr = profile.Profile.get_instance()
pr.accept_transfer(self, directory + '/' + self.saved_name, friend_number, file_number, size)
self.accept.setVisible(False)
@QtCore.Slot(int, float)
def update(self, state, progress):
self.pb.setValue(int(progress * 100))
if state == TOX_FILE_TRANSFER_STATE['CANCELED']:
self.setStyleSheet('QListWidget { background-color: red; }')
self.cancel.setVisible(False)
self.accept.setVisible(False)
self.pb.setVisible(False)
elif state == TOX_FILE_TRANSFER_STATE['FINISHED']:
self.pb.setVisible(False)
self.cancel.setVisible(False)

View file

@ -102,6 +102,7 @@ class MainWindow(QtGui.QMainWindow):
self.sendMessageButton.setGeometry(QtCore.QRect(550, 10, 60, 110)) self.sendMessageButton.setGeometry(QtCore.QRect(550, 10, 60, 110))
self.sendMessageButton.setObjectName("sendMessageButton") self.sendMessageButton.setObjectName("sendMessageButton")
self.sendMessageButton.clicked.connect(self.send_message) self.sendMessageButton.clicked.connect(self.send_message)
pixmap = QtGui.QPixmap(curr_directory() + '/images/send.png') pixmap = QtGui.QPixmap(curr_directory() + '/images/send.png')
icon = QtGui.QIcon(pixmap) icon = QtGui.QIcon(pixmap)
self.sendMessageButton.setIcon(icon) self.sendMessageButton.setIcon(icon)
@ -114,6 +115,9 @@ class MainWindow(QtGui.QMainWindow):
icon = QtGui.QIcon(pixmap) icon = QtGui.QIcon(pixmap)
self.screenshotButton.setIcon(icon) self.screenshotButton.setIcon(icon)
self.screenshotButton.setIconSize(QtCore.QSize(90, 40)) self.screenshotButton.setIconSize(QtCore.QSize(90, 40))
self.fileTransferButton.clicked.connect(self.send_file)
self.screenshotButton.clicked.connect(self.send_screenshot)
QtCore.QMetaObject.connectSlotsByName(Form) QtCore.QMetaObject.connectSlotsByName(Form)
def setup_left_bottom(self, Form): def setup_left_bottom(self, Form):
@ -277,51 +281,75 @@ class MainWindow(QtGui.QMainWindow):
self.int_s.show() self.int_s.show()
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Messages # Messages and file transfers
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def send_message(self): def send_message(self):
text = self.messageEdit.toPlainText() text = self.messageEdit.toPlainText()
self.profile.send_message(text) self.profile.send_message(text)
def send_file(self):
if self.profile.is_active_online(): # active friend exists and online
name = QtGui.QFileDialog.getOpenFileName(self, 'Choose file')
if name[0]:
self.profile.send_file(name[0])
def send_screenshot(self):
# TODO: add screenshots support
if self.profile.is_active_online(): # active friend exists and online
pass
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Functions which called when user open context menu in friends list # Functions which called when user open context menu in friends list
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def friend_right_click(self, pos): def friend_right_click(self, pos):
item = self.friends_list.itemAt(pos) item = self.friends_list.itemAt(pos)
num = self.friends_list.indexFromItem(item).row()
friend = Profile.get_instance().get_friend_by_number(num)
settings = Settings.get_instance()
allowed = friend.tox_id in settings['auto_accept_from_friends']
auto = 'Disallow auto accept' if allowed else 'Allow auto accept'
if item is not None: if item is not None:
self.listMenu = QtGui.QMenu() self.listMenu = QtGui.QMenu()
set_alias_item = self.listMenu.addAction('Set alias') set_alias_item = self.listMenu.addAction('Set alias')
clear_history_item = self.listMenu.addAction('Clear history') clear_history_item = self.listMenu.addAction('Clear history')
copy_key_item = self.listMenu.addAction('Copy public key') copy_key_item = self.listMenu.addAction('Copy public key')
auto_accept_item = self.listMenu.addAction(auto)
remove_item = self.listMenu.addAction('Remove friend') remove_item = self.listMenu.addAction('Remove friend')
self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(item)) self.connect(set_alias_item, QtCore.SIGNAL("triggered()"), lambda: self.set_alias(num))
self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(item)) self.connect(remove_item, QtCore.SIGNAL("triggered()"), lambda: self.remove_friend(num))
self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(item)) self.connect(copy_key_item, QtCore.SIGNAL("triggered()"), lambda: self.copy_friend_key(num))
self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(item)) self.connect(clear_history_item, QtCore.SIGNAL("triggered()"), lambda: self.clear_history(num))
self.connect(auto_accept_item, QtCore.SIGNAL("triggered()"), lambda: self.auto_accept(num, not allowed))
parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0)) parent_position = self.friends_list.mapToGlobal(QtCore.QPoint(0, 0))
self.listMenu.move(parent_position + pos) self.listMenu.move(parent_position + pos)
self.listMenu.show() self.listMenu.show()
def set_alias(self, item): def set_alias(self, num):
num = self.friends_list.indexFromItem(item).row()
self.profile.set_alias(num) self.profile.set_alias(num)
def remove_friend(self, item): def remove_friend(self, num):
num = self.friends_list.indexFromItem(item).row()
self.profile.delete_friend(num) self.profile.delete_friend(num)
def copy_friend_key(self, item): def copy_friend_key(self, num):
num = self.friends_list.indexFromItem(item).row()
tox_id = self.profile.friend_public_key(num) tox_id = self.profile.friend_public_key(num)
clipboard = QtGui.QApplication.clipboard() clipboard = QtGui.QApplication.clipboard()
clipboard.setText(tox_id) clipboard.setText(tox_id)
def clear_history(self, item): def clear_history(self, num):
num = self.friends_list.indexFromItem(item).row()
self.profile.clear_history(num) self.profile.clear_history(num)
def auto_accept(self, num, value):
settings = Settings.get_instance()
tox_id = self.profile.friend_public_key(num)
if value:
settings['auto_accept_from_friends'].append(tox_id)
else:
index = settings['auto_accept_from_friends'].index(tox_id)
del settings['auto_accept_from_friends'][index]
settings.save()
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Functions which called when user click somewhere else # Functions which called when user click somewhere else
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------

View file

@ -1,7 +1,7 @@
from PySide import QtCore, QtGui from PySide import QtCore, QtGui
from settings import Settings from settings import Settings
from profile import Profile, ProfileHelper from profile import Profile, ProfileHelper
from util import get_style from util import get_style, curr_directory
class CenteredWidget(QtGui.QWidget): class CenteredWidget(QtGui.QWidget):
@ -165,7 +165,7 @@ class ProfileSettings(CenteredWidget):
Profile.get_instance().reset_avatar() Profile.get_instance().reset_avatar()
def set_avatar(self): def set_avatar(self):
name = QtGui.QFileDialog.getOpenFileName(self, 'Open file') name = QtGui.QFileDialog.getOpenFileName(self, 'Open file', None, 'Image Files (*.png)')
print name print name
if name[0]: if name[0]:
with open(name[0], 'rb') as f: with open(name[0], 'rb') as f:
@ -174,11 +174,12 @@ class ProfileSettings(CenteredWidget):
def export_profile(self): def export_profile(self):
directory = QtGui.QFileDialog.getExistingDirectory() + '/' directory = QtGui.QFileDialog.getExistingDirectory() + '/'
ProfileHelper.export_profile(directory) if directory != '/':
settings = Settings.get_instance() ProfileHelper.export_profile(directory)
settings.export(directory) settings = Settings.get_instance()
profile = Profile.get_instance() settings.export(directory)
profile.export_history(directory) profile = Profile.get_instance()
profile.export_history(directory)
def closeEvent(self, event): def closeEvent(self, event):
profile = Profile.get_instance() profile = Profile.get_instance()
@ -275,22 +276,30 @@ class PrivacySettings(CenteredWidget):
self.fileautoaccept.setGeometry(QtCore.QRect(40, 60, 271, 22)) self.fileautoaccept.setGeometry(QtCore.QRect(40, 60, 271, 22))
self.fileautoaccept.setObjectName("fileautoaccept") self.fileautoaccept.setObjectName("fileautoaccept")
self.typingNotifications = QtGui.QCheckBox(self) self.typingNotifications = QtGui.QCheckBox(self)
self.typingNotifications.setGeometry(QtCore.QRect(40, 90, 350, 31)) self.typingNotifications.setGeometry(QtCore.QRect(40, 90, 350, 30))
self.typingNotifications.setBaseSize(QtCore.QSize(350, 200))
self.typingNotifications.setObjectName("typingNotifications") self.typingNotifications.setObjectName("typingNotifications")
self.auto_path = QtGui.QLabel(self)
self.auto_path.setGeometry(QtCore.QRect(40, 120, 350, 30))
self.path = QtGui.QPlainTextEdit(self)
self.path.setGeometry(QtCore.QRect(10, 160, 330, 30))
self.change_path = QtGui.QPushButton(self)
self.change_path.setGeometry(QtCore.QRect(230, 120, 100, 30))
self.retranslateUi() self.retranslateUi()
settings = Settings.get_instance() settings = Settings.get_instance()
self.typingNotifications.setChecked(settings['typing_notifications']) self.typingNotifications.setChecked(settings['typing_notifications'])
self.fileautoaccept.setChecked(settings['allow_auto_accept']) self.fileautoaccept.setChecked(settings['allow_auto_accept'])
self.saveHistory.setChecked(settings['save_history']) self.saveHistory.setChecked(settings['save_history'])
self.path.setPlainText(settings['auto_accept_path'] or curr_directory())
self.change_path.clicked.connect(self.new_path)
QtCore.QMetaObject.connectSlotsByName(self) QtCore.QMetaObject.connectSlotsByName(self)
def retranslateUi(self): def retranslateUi(self):
self.setWindowTitle(QtGui.QApplication.translate("privacySettings", "Privacy settings", None, QtGui.QApplication.UnicodeUTF8)) self.setWindowTitle(QtGui.QApplication.translate("privacySettings", "Privacy settings", None, QtGui.QApplication.UnicodeUTF8))
self.saveHistory.setText(QtGui.QApplication.translate("privacySettings", "Save chat history", None, QtGui.QApplication.UnicodeUTF8)) self.saveHistory.setText(QtGui.QApplication.translate("privacySettings", "Save chat history", None, QtGui.QApplication.UnicodeUTF8))
self.fileautoaccept.setText(QtGui.QApplication.translate("privacySettings", "Allow file autoaccept", None, QtGui.QApplication.UnicodeUTF8)) self.fileautoaccept.setText(QtGui.QApplication.translate("privacySettings", "Allow file auto accept", None, QtGui.QApplication.UnicodeUTF8))
self.typingNotifications.setText(QtGui.QApplication.translate("privacySettings", "Send typing notifications", None, QtGui.QApplication.UnicodeUTF8)) self.typingNotifications.setText(QtGui.QApplication.translate("privacySettings", "Send typing notifications", None, QtGui.QApplication.UnicodeUTF8))
self.auto_path.setText(QtGui.QApplication.translate("privacySettings", "Auto accept default path:", None, QtGui.QApplication.UnicodeUTF8))
self.change_path.setText(QtGui.QApplication.translate("privacySettings", "Change", None, QtGui.QApplication.UnicodeUTF8))
def closeEvent(self, event): def closeEvent(self, event):
settings = Settings.get_instance() settings = Settings.get_instance()
@ -299,8 +308,14 @@ class PrivacySettings(CenteredWidget):
if settings['save_history'] and not self.saveHistory.isChecked(): # clear history if settings['save_history'] and not self.saveHistory.isChecked(): # clear history
Profile.get_instance().clear_history() Profile.get_instance().clear_history()
settings['save_history'] = self.saveHistory.isChecked() settings['save_history'] = self.saveHistory.isChecked()
settings['auto_accept_path'] = self.path.toPlainText()
settings.save() settings.save()
def new_path(self):
directory = QtGui.QFileDialog.getExistingDirectory() + '/'
if directory != '/':
self.path.setPlainText(directory)
class NotificationsSettings(CenteredWidget): class NotificationsSettings(CenteredWidget):
"""Notifications settings form""" """Notifications settings form"""

View file

@ -2,6 +2,7 @@ from PySide import QtGui
from PySide.phonon import Phonon from PySide.phonon import Phonon
from util import curr_directory from util import curr_directory
# TODO: make app icon active # TODO: make app icon active
# TODO: add all sound notifications
SOUND_NOTIFICATION = { SOUND_NOTIFICATION = {
@ -19,9 +20,10 @@ def tray_notification(title, text, tray):
def sound_notification(t): def sound_notification(t):
# TODO: add other sound notifications
if t == SOUND_NOTIFICATION['MESSAGE']: if t == SOUND_NOTIFICATION['MESSAGE']:
f = curr_directory() + '/sounds/message.wav' f = curr_directory() + '/sounds/message.wav'
elif t == SOUND_NOTIFICATION['FILE_TRANSFER']:
f = curr_directory() + '/sounds/file.wav'
else: else:
return return
m = Phonon.MediaSource(f) m = Phonon.MediaSource(f)

View file

@ -1,4 +1,4 @@
from list_items import MessageItem, ContactItem from list_items import MessageItem, ContactItem, FileTransferItem
from PySide import QtCore, QtGui from PySide import QtCore, QtGui
from tox import Tox from tox import Tox
import os import os
@ -8,6 +8,7 @@ from ctypes import *
from util import curr_time, log, Singleton, curr_directory, convert_time from util import curr_time, log, Singleton, curr_directory, convert_time
from tox_dns import tox_dns from tox_dns import tox_dns
from history import * from history import *
from file_transfers import *
import time import time
@ -56,7 +57,7 @@ class ProfileHelper(object):
@staticmethod @staticmethod
def export_profile(new_path): def export_profile(new_path):
new_path += ProfileHelper._path.split('/')[-1] new_path += os.path.basename(ProfileHelper._path)
with open(ProfileHelper._path, 'rb') as fin: with open(ProfileHelper._path, 'rb') as fin:
data = fin.read() data = fin.read()
with open(new_path, 'wb') as fout: with open(new_path, 'wb') as fout:
@ -161,7 +162,7 @@ class Contact(object):
""" """
Tries to load avatar of contact or uses default avatar Tries to load avatar of contact or uses default avatar
""" """
avatar_path = (Settings.get_default_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
if not os.path.isfile(avatar_path): # load default image if not os.path.isfile(avatar_path): # load default image
avatar_path = curr_directory() + '/images/avatar.png' avatar_path = curr_directory() + '/images/avatar.png'
pixmap = QtGui.QPixmap(QtCore.QSize(64, 64)) pixmap = QtGui.QPixmap(QtCore.QSize(64, 64))
@ -170,17 +171,25 @@ class Contact(object):
self._widget.avatar_label.repaint() self._widget.avatar_label.repaint()
def reset_avatar(self): def reset_avatar(self):
avatar_path = (Settings.get_default_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
if os.path.isfile(avatar_path): if os.path.isfile(avatar_path):
os.remove(avatar_path) os.remove(avatar_path)
self.load_avatar() self.load_avatar()
def set_avatar(self, avatar): def set_avatar(self, avatar):
avatar_path = (Settings.get_default_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2]) avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
with open(avatar_path, 'wb') as f: with open(avatar_path, 'wb') as f:
f.write(avatar) f.write(avatar)
self.load_avatar() self.load_avatar()
# def get_avatar_hash(self):
# avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
# if not os.path.isfile(avatar_path): # load default image
# return 0
# with open(avatar_path, 'rb') as fl:
# data = fl.read()
# return Tox.hash(data)
class Friend(Contact): class Friend(Contact):
""" """
@ -318,6 +327,7 @@ class Profile(Contact, Singleton):
self._screen = screen self._screen = screen
self._messages = screen.messages self._messages = screen.messages
self._tox = tox self._tox = tox
self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number)
settings = Settings.get_instance() settings = Settings.get_instance()
self._show_online = settings['show_online_friends'] self._show_online = settings['show_online_friends']
screen.online_contacts.setChecked(self._show_online) screen.online_contacts.setChecked(self._show_online)
@ -497,7 +507,7 @@ class Profile(Contact, Singleton):
self._messages.scrollToBottom() self._messages.scrollToBottom()
self._friends[self._active_friend].append_message((message.decode('utf-8'), self._friends[self._active_friend].append_message((message.decode('utf-8'),
MESSAGE_OWNER['FRIEND'], MESSAGE_OWNER['FRIEND'],
time.time(), int(time.time()),
message_type)) message_type))
else: else:
friend = filter(lambda x: x.number == friend_num, self._friends)[0] friend = filter(lambda x: x.number == friend_num, self._friends)[0]
@ -537,11 +547,12 @@ class Profile(Contact, Singleton):
Save history to db Save history to db
""" """
print 'In save' print 'In save'
if Settings.get_instance()['save_history']: if hasattr(self, '_history'):
for friend in self._friends: if Settings.get_instance()['save_history']:
messages = friend.get_corr_for_saving() for friend in self._friends:
self._history.save_messages_to_db(friend.tox_id, messages) messages = friend.get_corr_for_saving()
del self._history self._history.save_messages_to_db(friend.tox_id, messages)
del self._history
def clear_history(self, num=None): def clear_history(self, num=None):
if num is not None: if num is not None:
@ -560,7 +571,7 @@ class Profile(Contact, Singleton):
self._history.export(directory) self._history.export(directory)
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Factories for friend and message items # Factories for friend, message and file transfer items
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
def create_friend_item(self): def create_friend_item(self):
@ -578,11 +589,21 @@ class Profile(Contact, Singleton):
def create_message_item(self, text, time, name, message_type): def create_message_item(self, text, time, name, message_type):
item = MessageItem(text, time, name, message_type, self._messages) item = MessageItem(text, time, name, message_type, self._messages)
elem = QtGui.QListWidgetItem(self._messages) elem = QtGui.QListWidgetItem(self._messages)
elem.setSizeHint(QtCore.QSize(500, item.getHeight())) elem.setSizeHint(QtCore.QSize(600, item.getHeight()))
self._messages.addItem(elem) self._messages.addItem(elem)
self._messages.setItemWidget(elem, item) self._messages.setItemWidget(elem, item)
self._messages.repaint() self._messages.repaint()
def create_file_transfer_item(self, file_name, size, friend_number, file_number, show_accept):
friend = self.get_friend_by_number(friend_number)
item = FileTransferItem(file_name, size, curr_time(), friend.name, friend_number, file_number, show_accept)
elem = QtGui.QListWidgetItem(self._messages)
elem.setSizeHint(QtCore.QSize(600, 50))
self._messages.addItem(elem)
self._messages.setItemWidget(elem, item)
self._messages.repaint()
return item
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
# Work with friends (remove, set alias, get public key) # Work with friends (remove, set alias, get public key)
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -701,6 +722,86 @@ class Profile(Contact, Singleton):
self.status = None self.status = None
for friend in self._friends: for friend in self._friends:
friend.status = None friend.status = None
# TODO: FT reset
# -----------------------------------------------------------------------------------------------------------------
# File transfers support
# -----------------------------------------------------------------------------------------------------------------
def incoming_file_transfer(self, friend_number, file_number, size, file_name):
settings = Settings.get_instance()
friend = self.get_friend_by_number(friend_number)
if settings['allow_auto_accept'] and friend.tox_id in settings['auto_accept_from_friends']:
path = settings['auto_accept_path'] or curr_directory()
self.accept_transfer(path + '/' + file_name.decode('utf-8'), friend_number, file_number)
self.create_file_transfer_item(file_name.decode('utf-8'), size, friend_number, file_number, False)
else:
self.create_file_transfer_item(file_name.decode('utf-8'), size, friend_number, file_number, True)
def cancel_transfer(self, friend_number, file_number):
if (friend_number, file_number) in self._file_transfers:
tr = self._file_transfers[(friend_number, file_number)]
tr.cancel()
del self._file_transfers[(friend_number, file_number)]
def accept_transfer(self, item, path, friend_number, file_number, size):
rt = ReceiveTransfer(path, self._tox, friend_number, size, file_number)
self._file_transfers[(friend_number, file_number)] = rt
self._tox.file_control(friend_number, file_number, TOX_FILE_CONTROL['RESUME'])
rt.set_state_changed_handler(item.update)
def incoming_avatar(self, friend_number, file_number, size):
"""
Friend changed avatar
:param friend_number: friend number
:param file_number: file number
:param size: size of avatar or 0 (default avatar)
"""
ra = ReceiveAvatar(self._tox, friend_number, size, file_number)
if ra.state != TOX_FILE_TRANSFER_STATE['CANCELED']:
self._file_transfers[(friend_number, file_number)] = ra
else:
self.get_friend_by_number(friend_number).load_avatar()
def incoming_chunk(self, friend_number, file_number, position, data):
if (friend_number, file_number) in self._file_transfers:
transfer = self._file_transfers[(friend_number, file_number)]
transfer.write_chunk(position, data)
if transfer.state:
if type(transfer) is ReceiveAvatar:
self.get_friend_by_number(friend_number).load_avatar()
del self._file_transfers[(friend_number, file_number)]
def send_avatar(self, friend_number):
avatar_path = (ProfileHelper.get_path() + 'avatars/{}.png').format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])
if not os.path.isfile(avatar_path): # reset image
avatar_path = None
sa = SendAvatar(avatar_path, self._tox, friend_number)
self._file_transfers[(friend_number, sa.get_file_number())] = sa
def send_file(self, path):
friend_number = self.get_active_number()
st = SendTransfer(path, self._tox, friend_number)
self._file_transfers[(friend_number, st.get_file_number())] = st
item = self.create_file_transfer_item(os.path.basename(path), os.path.getsize(path), friend_number, st.get_file_number(), False)
st.set_state_changed_handler(item.update)
def outgoing_chunk(self, friend_number, file_number, position, size):
if (friend_number, file_number) in self._file_transfers:
transfer = self._file_transfers[(friend_number, file_number)]
transfer.send_chunk(position, size)
if transfer.state:
del self._file_transfers[(friend_number, file_number)]
def reset_avatar(self):
super(Profile, self).reset_avatar()
for friend in filter(lambda x: x.status is not None, self._friends):
self.send_avatar(friend.number)
def set_avatar(self, data):
super(Profile, self).set_avatar(data)
for friend in filter(lambda x: x.status is not None, self._friends):
self.send_avatar(friend.number)
def tox_factory(data=None, settings=None): def tox_factory(data=None, settings=None):

View file

@ -53,6 +53,7 @@ class Settings(Singleton, dict):
'save_history': False, 'save_history': False,
'allow_inline': True, 'allow_inline': True,
'allow_auto_accept': False, 'allow_auto_accept': False,
'auto_accept_path': None,
'show_online_friends': False, 'show_online_friends': False,
'auto_accept_from_friends': [], 'auto_accept_from_friends': [],
'friends_aliases': [], 'friends_aliases': [],

BIN
src/sounds/file.wav Normal file

Binary file not shown.

BIN
src/sounds/message.wav Normal file

Binary file not shown.

View file

@ -1239,4 +1239,9 @@ MessageItem::focus
MessageEdit:hover MessageEdit:hover
{ {
border: none; border: none;
}
QListWidget QPushButton
{
background-color: transparent;
} }

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from ctypes import c_char_p, Structure, CDLL, c_bool, addressof, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 from ctypes import c_char_p, Structure, CDLL, c_bool, addressof, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64
from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8
from platform import system from platform import system
from toxcore_enums_and_consts import * from toxcore_enums_and_consts import *
@ -36,7 +36,7 @@ class LibToxCore(object):
def string_to_bin(tox_id): def string_to_bin(tox_id):
return c_char_p(tox_id.decode('hex')) return c_char_p(tox_id.decode('hex')) if tox_id is not None else None
def bin_to_string(raw_id, length): def bin_to_string(raw_id, length):
@ -1351,7 +1351,7 @@ class Tox(object):
pointer (c_void_p) to user_data pointer (c_void_p) to user_data
:param user_data: pointer (c_void_p) to user data :param user_data: pointer (c_void_p) to user data
""" """
c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, c_char_p, c_size_t, c_void_p) c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_uint64, POINTER(c_uint8), c_size_t, c_void_p)
self.file_recv_chunk_cb = c_callback(callback) self.file_recv_chunk_cb = c_callback(callback)
self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb, user_data) self.libtoxcore.tox_callback_file_recv_chunk(self._tox_pointer, self.file_recv_chunk_cb, user_data)

View file

@ -3,7 +3,7 @@ import time
from platform import system from platform import system
program_version = '0.0.1 (alpha)' program_version = '0.0.2 (alpha)'
def log(data): def log(data):