diff --git a/toxygen/app.py b/toxygen/app.py index e038980..8dd4ea8 100644 --- a/toxygen/app.py +++ b/toxygen/app.py @@ -15,6 +15,11 @@ from plugin_support.plugin_support import PluginLoader from ui.main_screen import MainWindow from ui import tray import util.ui as util_ui +import util.util as util +from contacts.profile import Profile +from file_transfers.file_transfers_handler import FileTransfersHandler +from contacts.contact_provider import ContactProvider +from contacts.friend_factory import FriendFactory class App: @@ -23,7 +28,7 @@ class App: self._version = version self._app = self._settings = self._profile_manager = self._plugin_loader = None self._tox = self._ms = self._init = self._main_loop = self._av_loop = None - self._uri = self._toxes = self._tray = None + self._uri = self._toxes = self._tray = self._file_transfer_handler = self._contacts_provider = None if uri is not None and uri.startswith('tox:'): self._uri = uri[4:] self._path = path_to_profile @@ -93,7 +98,11 @@ class App: return self._ms = MainWindow(self._settings, self._tox, self.reset, self._tray) - profile = self._ms.profile + self._friend_factory = FriendFactory(None, self._profile_manager, self._settings, self._tox) + self._contacts_provider = ContactProvider(self._tox, self._friend_factory) + self._file_transfer_handler = FileTransfersHandler(self._tox, self._settings, self._contacts_provider) + profile = Profile(self._profile_manager, self._tox, self._ms, self._file_transfer_handler) + self._ms.profile = profile self._ms.show() self._tray = tray.init_tray(profile, self._settings, self._ms) @@ -110,7 +119,17 @@ class App: self._ms.add_contact(self._uri) self._app.lastWindowClosed.connect(self._app.quit) - self._app.exec_() + # main + while True: + try: + self._app.exec_() + except KeyboardInterrupt: + print('Closing Toxygen...') + break + except Exception as ex: + util.log('Unhandled exception: ' + str(ex)) + else: + break self._plugin_loader.stop() self.stop_threads() @@ -132,7 +151,7 @@ class App: self._tox = self.create_tox(data) self.start_threads() - self._plugin_loader.set_tox(self._tox) + # TODO: foreach in list of tox savers set_tox return self._tox @@ -178,7 +197,7 @@ class App: def start_threads(self): # init thread - self._init = threads.InitThread(self._tox, self._plugin_loader) + self._init = threads.InitThread(self._tox, self._plugin_loader, self._settings) self._init.start() # starting threads for tox iterate and toxav iterate diff --git a/toxygen/bootstrap/bootstrap.py b/toxygen/bootstrap/bootstrap.py index 87c6d9c..aa6f863 100644 --- a/toxygen/bootstrap/bootstrap.py +++ b/toxygen/bootstrap/bootstrap.py @@ -38,13 +38,12 @@ def save_nodes(nodes): fl.write(nodes) -def download_nodes_list(): +def download_nodes_list(settings): url = 'https://nodes.tox.chat/json' - s = settings.Settings.get_instance() - if not s['download_nodes_list']: + if not settings['download_nodes_list']: return - if not s['proxy_type']: # no proxy + if not settings['proxy_type']: # no proxy try: req = urllib.request.Request(url) req.add_header('Content-Type', 'application/json') @@ -57,9 +56,9 @@ def download_nodes_list(): netman = QtNetwork.QNetworkAccessManager() proxy = QtNetwork.QNetworkProxy() proxy.setType( - QtNetwork.QNetworkProxy.Socks5Proxy if s['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) - proxy.setHostName(s['proxy_host']) - proxy.setPort(s['proxy_port']) + QtNetwork.QNetworkProxy.Socks5Proxy if settings['proxy_type'] == 2 else QtNetwork.QNetworkProxy.HttpProxy) + proxy.setHostName(settings['proxy_host']) + proxy.setPort(settings['proxy_port']) netman.setProxy(proxy) try: request = QtNetwork.QNetworkRequest() diff --git a/toxygen/contacts/basecontact.py b/toxygen/contacts/basecontact.py index da992de..71690dd 100644 --- a/toxygen/contacts/basecontact.py +++ b/toxygen/contacts/basecontact.py @@ -106,7 +106,7 @@ class BaseContact: return self._widget.avatar_label.pixmap() def get_avatar_path(self): - directory = util.join_path(self._profile_manager.get_path(), 'avatars') + directory = util.join_path(self._profile_manager.get_dir(), 'avatars') avatar_path = util.join_path(directory, '{}.png'.format(self._tox_id[:TOX_PUBLIC_KEY_SIZE * 2])) if not os.path.isfile(avatar_path) or not os.path.getsize(avatar_path): # load default image avatar_path = util.join_path(util.get_images_directory(), self.get_default_avatar_name()) diff --git a/toxygen/contacts/contact_provider.py b/toxygen/contacts/contact_provider.py new file mode 100644 index 0000000..10b778b --- /dev/null +++ b/toxygen/contacts/contact_provider.py @@ -0,0 +1,38 @@ +import util.util as util + + +class ContactProvider(util.ToxSave): + + def __init__(self, tox, friend_factory): + super().__init__(tox) + self._friend_factory = friend_factory + self._cache = {} # key - contact's public key, value - contact instance + + def get_friend_by_number(self, friend_number): + public_key = self._tox.friend_get_public_key(friend_number) + + return self.get_friend_by_public_key(public_key) + + def get_friend_by_public_key(self, public_key): + friend = self._get_contact_from_cache(public_key) + if friend is not None: + return friend + friend = self._friend_factory.create_friend_by_public_key(public_key) + self._add_to_cache(public_key, friend) + + return friend + + def get_gc_by_number(self): + pass + + def get_gc_by_public_key(self): + pass + + def clear_cache(self): + self._cache.clear() + + def _get_contact_from_cache(self, public_key): + return self._cache[public_key] if public_key in self._cache else None + + def _add_to_cache(self, public_key, contact): + self._cache[public_key] = contact diff --git a/toxygen/contacts/friend_factory.py b/toxygen/contacts/friend_factory.py new file mode 100644 index 0000000..556f2d4 --- /dev/null +++ b/toxygen/contacts/friend_factory.py @@ -0,0 +1,38 @@ +from contacts.friend import Friend + + +class FriendFactory: + + def __init__(self, history, profile_manager, settings, tox): + self._history, self._profile_manager = history, profile_manager + self._settings, self._tox = settings, tox + + def create_friend_by_number(self, friend_number): + aliases = self._settings['friends_aliases'] + tox_id = self._tox.friend_get_public_key(friend_number) + try: + alias = list(filter(lambda x: x[0] == tox_id, aliases))[0][1] + except: + alias = '' + item = self.create_friend_item() + name = alias or self._tox.friend_get_name(friend_number) or tox_id + status_message = self._tox.friend_get_status_message(friend_number) + if not self._history.friend_exists_in_db(tox_id): + self._history.add_friend_to_db(tox_id) + message_getter = self._history.messages_getter(tox_id) + friend = Friend(self._profile_manager, message_getter, friend_number, name, status_message, item, tox_id) + friend.set_alias(alias) + + return friend + + def create_friend_by_public_key(self, public_key): + friend_number = self._tox.friend_by_public_key(public_key) + + return self.create_friend_by_number(friend_number) + + def create_friend_item(self): + """ + Method-factory + :return: new widget for friend instance + """ + return self._factory.friend_item() diff --git a/toxygen/contacts/profile.py b/toxygen/contacts/profile.py index 4181753..0bfc764 100644 --- a/toxygen/contacts/profile.py +++ b/toxygen/contacts/profile.py @@ -40,7 +40,8 @@ class Profile(basecontact.BaseContact): self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number) self._load_history = True self._waiting_for_reconnection = False - self._factory = items_factory.ItemsFactory(self._screen.friends_list, self._messages) + #self._factory = items_factory.ItemsFactory(self._screen.friends_list, self._messages) + self._contacts_manager = None #self._show_avatars = settings['show_avatars'] # ----------------------------------------------------------------------------------------------------------------- @@ -77,7 +78,7 @@ class Profile(basecontact.BaseContact): self._messages.scrollToBottom() def set_status_message(self, value): - super(Profile, self).set_status_message(value) + super().set_status_message(value) self._tox.self_set_status_message(self._status_message.encode('utf-8')) def new_nospam(self): @@ -85,6 +86,7 @@ class Profile(basecontact.BaseContact): import random self._tox.self_set_nospam(random.randint(0, 4294967295)) # no spam - uint32 self._tox_id = self._tox.self_get_address() + return self._tox_id # ----------------------------------------------------------------------------------------------------------------- @@ -296,7 +298,7 @@ class Profile(basecontact.BaseContact): self.status = None for friend in self._contacts: friend.number = self._tox.friend_by_public_key(friend.tox_id) # numbers update - self.update_filtration() + self._contacts_manager.update_filtration() def reconnect(self): self._waiting_for_reconnection = False diff --git a/toxygen/file_transfers/file_transfers.py b/toxygen/file_transfers/file_transfers.py index 6c65809..5aab6f9 100644 --- a/toxygen/file_transfers/file_transfers.py +++ b/toxygen/file_transfers/file_transfers.py @@ -48,7 +48,7 @@ class FileTransfer(QtCore.QObject): """ def __init__(self, path, tox, friend_number, size, file_number=None): - QtCore.QObject.__init__(self) + super().__init__(self) self._path = path self._tox = tox self._friend_number = friend_number @@ -134,7 +134,7 @@ class SendTransfer(FileTransfer): size = getsize(path) else: size = 0 - super(SendTransfer, self).__init__(path, tox, friend_number, size) + super().__init__(path, tox, friend_number, size) self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] self._file_number = tox.file_send(friend_number, kind, size, file_id, bytes(basename(path), 'utf-8') if path else b'') @@ -168,11 +168,11 @@ class SendAvatar(SendTransfer): def __init__(self, path, tox, friend_number): if path is None: - hash = None + avatar_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) + avatar_hash = Tox.hash(fl.read()) + super(SendAvatar, self).__init__(path, tox, friend_number, TOX_FILE_KIND['AVATAR'], avatar_hash) class SendFromBuffer(FileTransfer): @@ -181,7 +181,7 @@ class SendFromBuffer(FileTransfer): """ def __init__(self, tox, friend_number, data, file_name): - super(SendFromBuffer, self).__init__(None, tox, friend_number, len(data)) + super().__init__(None, tox, friend_number, len(data)) self.state = TOX_FILE_TRANSFER_STATE['OUTGOING_NOT_STARTED'] self._data = data self._file_number = tox.file_send(friend_number, TOX_FILE_KIND['DATA'], @@ -206,10 +206,10 @@ class SendFromBuffer(FileTransfer): class SendFromFileBuffer(SendTransfer): def __init__(self, *args): - super(SendFromFileBuffer, self).__init__(*args) + super().__init__(*args) def send_chunk(self, position, size): - super(SendFromFileBuffer, self).send_chunk(position, size) + super().send_chunk(position, size) if not size: chdir(dirname(self._path)) remove(self._path) @@ -222,7 +222,7 @@ class SendFromFileBuffer(SendTransfer): class ReceiveTransfer(FileTransfer): def __init__(self, path, tox, friend_number, size, file_number, position=0): - super(ReceiveTransfer, self).__init__(path, tox, friend_number, size, file_number) + super().__init__(path, tox, friend_number, size, file_number) self._file = open(self._path, 'wb') self._file_size = position self._file.truncate(position) @@ -231,11 +231,12 @@ class ReceiveTransfer(FileTransfer): self._done = position def cancel(self): - super(ReceiveTransfer, self).cancel() + super().cancel() remove(self._path) def total_size(self): self._missed.add(self._file_size) + return min(self._missed) def write_chunk(self, position, data): @@ -273,7 +274,7 @@ class ReceiveToBuffer(FileTransfer): """ def __init__(self, tox, friend_number, size, file_number): - super(ReceiveToBuffer, self).__init__(None, tox, friend_number, size, file_number) + super().__init__(None, tox, friend_number, size, file_number) self._data = bytes() self._data_size = 0 @@ -306,7 +307,7 @@ class ReceiveAvatar(ReceiveTransfer): def __init__(self, tox, friend_number, size, file_number): path = settings.ProfileManager.get_path() + 'avatars/{}.png'.format(tox.friend_get_public_key(friend_number)) - super(ReceiveAvatar, self).__init__(path + '.tmp', tox, friend_number, size, file_number) + super().__init__(path + '.tmp', tox, friend_number, size, file_number) if size > self.MAX_AVATAR_SIZE: self.send_control(TOX_FILE_CONTROL['CANCEL']) self._file.close() @@ -333,7 +334,7 @@ class ReceiveAvatar(ReceiveTransfer): self.send_control(TOX_FILE_CONTROL['RESUME']) def write_chunk(self, position, data): - super(ReceiveAvatar, self).write_chunk(position, data) + super().write_chunk(position, data) if self.state: avatar_path = self._path[:-4] if exists(avatar_path): diff --git a/toxygen/file_transfers/file_transfers_handler.py b/toxygen/file_transfers/file_transfers_handler.py index 6431fe4..11c47c5 100644 --- a/toxygen/file_transfers/file_transfers_handler.py +++ b/toxygen/file_transfers/file_transfers_handler.py @@ -1,14 +1,18 @@ from file_transfers.file_transfers import * from messenger.messages import * +from history.database import MESSAGE_OWNER import os +import util.util as util class FileTransfersHandler: - def __init__(self, tox, settings): + def __init__(self, tox, settings, contact_provider): self._tox = tox self._settings = settings + self._contact_provider = contact_provider self._file_transfers = {} + # key = (friend number, file number), value - transfer instance self._paused_file_transfers = dict(settings['paused_file_transfers']) # key - file id, value: [path, friend number, is incoming, start position] @@ -24,9 +28,9 @@ class FileTransfersHandler: :param size: file size in bytes :param file_name: file name without path """ - friend = self.get_friend_by_number(friend_number) - auto = self._settings['allow_auto_accept'] and friend.tox_id in settings['auto_accept_from_friends'] - inline = is_inline(file_name) and settings['allow_inline'] + friend = self._get_friend_by_number(friend_number) + auto = self._settings['allow_auto_accept'] and friend.tox_id in self._settings['auto_accept_from_friends'] + inline = is_inline(file_name) and self._settings['allow_inline'] file_id = self._tox.file_get_file_id(friend_number, file_number) accepted = True if file_id in self._paused_file_transfers: @@ -55,7 +59,7 @@ class FileTransfersHandler: file_number) elif auto: - path = settings['auto_accept_path'] or curr_directory() + path = self._settings['auto_accept_path'] or util.curr_directory() self.accept_transfer(None, path + '/' + file_name, friend_number, file_number, size) tm = TransferMessage(MESSAGE_OWNER['FRIEND'], time.time(), @@ -90,7 +94,7 @@ class FileTransfersHandler: :param file_number: file number :param already_cancelled: was cancelled by friend """ - i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, + i = self._get_friend_by_number(friend_number).update_transfer_data(file_number, TOX_FILE_TRANSFER_STATE['CANCELLED']) if (friend_number, file_number) in self._file_transfers: tr = self._file_transfers[(friend_number, file_number)] @@ -128,8 +132,8 @@ class FileTransfersHandler: """ Resume transfer with specified data """ - self.get_friend_by_number(friend_number).update_transfer_data(file_number, - TOX_FILE_TRANSFER_STATE['RUNNING']) + # self.get_friend_by_number(friend_number).update_transfer_data(file_number, + # TOX_FILE_TRANSFER_STATE['RUNNING']) tr = self._file_transfers[(friend_number, file_number)] if by_friend: tr.state = TOX_FILE_TRANSFER_STATE['RUNNING'] @@ -261,7 +265,7 @@ class FileTransfersHandler: self.get_friend_by_number(friend_number).load_avatar() if friend_number == self.get_active_number() and self.is_active_a_friend(): self.set_active(None) - elif t is ReceiveToBuffer or (t is SendFromBuffer and Settings.get_instance()['allow_inline']): # inline image + elif t is ReceiveToBuffer or (t is SendFromBuffer and self._settings.get_instance()['allow_inline']): # inline image print('inline') inline = InlineImage(transfer.get_data()) i = self.get_friend_by_number(friend_number).update_transfer_data(file_number, @@ -286,13 +290,10 @@ class FileTransfersHandler: # Avatars support # ----------------------------------------------------------------------------------------------------------------- - def send_avatar(self, friend_number): + def send_avatar(self, friend_number, avatar_path=None): """ :param friend_number: number of friend who should get new avatar """ - avatar_path = (ProfileManager.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 @@ -311,3 +312,6 @@ class FileTransfersHandler: self.get_friend_by_number(friend_number).load_avatar() if self.get_active_number() == friend_number and self.is_active_a_friend(): self.set_active(None) + + def _get_friend_by_number(self, friend_number): + return self._contact_provider.get_friend_by_number(friend_number) diff --git a/toxygen/history/database.py b/toxygen/history/database.py index ff9ce8e..9960b54 100644 --- a/toxygen/history/database.py +++ b/toxygen/history/database.py @@ -2,7 +2,6 @@ from sqlite3 import connect from user_data import settings from os import chdir import os.path -from user_data.toxes import ToxES PAGE_SIZE = 42 @@ -14,10 +13,11 @@ SAVE_MESSAGES = 500 MESSAGE_OWNER = { 'ME': 0, 'FRIEND': 1, - 'NOT_SENT': 2 + 'NOT_SENT': 2, + 'GC_PEER': 3 } -# TODO: unique message id and ngc support, db name as profile name +# TODO: unique message id and ngc support, profile name as db name class Database: @@ -58,9 +58,8 @@ class Database: new_path = directory + self._name + '.hstr' with open(path, 'rb') as fin: data = fin.read() - encr = ToxES.get_instance() - if encr.has_password(): - data = encr.pass_encrypt(data) + if self._toxes.has_password(): + data = self._toxes.pass_encrypt(data) with open(new_path, 'wb') as fout: fout.write(data) diff --git a/toxygen/messenger/messages.py b/toxygen/messenger/messages.py index 4baa29a..e8abec7 100644 --- a/toxygen/messenger/messages.py +++ b/toxygen/messenger/messages.py @@ -1,3 +1,4 @@ +from history.database import MESSAGE_OWNER MESSAGE_TYPE = { @@ -9,28 +10,54 @@ MESSAGE_TYPE = { } +class MessageAuthor: + + def __init__(self, author_name, author_type): + self.name = author_name + self.type = author_type + + class Message: - def __init__(self, message_id, message_type, owner, time): + def __init__(self, message_id, message_type, author, time): self._time = time self._type = message_type - self._owner = owner + self._author = author self._message_id = message_id + self._widget = None def get_type(self): return self._type - def get_owner(self): - return self._owner + type = property(get_type) - def mark_as_sent(self): - self._owner = 0 + def get_author(self): + return self._author + + author = property(get_author) def get_message_id(self): return self._message_id message_id = property(get_message_id) + def get_widget(self): + if self._widget is None: + self._widget = self._create_widget() + + return self._widget + + widget = property(get_widget) + + def remove_widget(self): + self._widget = None + + def mark_as_sent(self): + self._author.author_type = MESSAGE_OWNER['ME'] + + def _create_widget(self): + pass + class TextMessage(Message): """ @@ -38,12 +65,15 @@ class TextMessage(Message): """ def __init__(self, id, message, owner, time, message_type): - super(TextMessage, self).__init__(id, message_type, owner, time) + super().__init__(id, message_type, owner, time) self._message = message def get_data(self): return self._message, self._owner, self._time, self._type + def _create_widget(self): + return + class GroupChatMessage(TextMessage): @@ -61,7 +91,7 @@ class TransferMessage(Message): """ def __init__(self, id, owner, time, status, size, name, friend_number, file_number): - super(TransferMessage, self).__init__(MESSAGE_TYPE['FILE_TRANSFER'], owner, time) + super().__init__(MESSAGE_TYPE['FILE_TRANSFER'], owner, time) self._status = status self._size = size self._file_name = name @@ -88,7 +118,7 @@ class TransferMessage(Message): class UnsentFile(Message): def __init__(self, id, path, data, time): - super(UnsentFile, self).__init__(id, MESSAGE_TYPE['FILE_TRANSFER'], 0, time) + super().__init__(id, MESSAGE_TYPE['FILE_TRANSFER'], 0, time) self._data, self._path = data, path def get_data(self): @@ -104,7 +134,7 @@ class InlineImage(Message): """ def __init__(self, id, data): - super(InlineImage, self).__init__(id, MESSAGE_TYPE['INLINE'], None, None) + super().__init__(id, MESSAGE_TYPE['INLINE'], None, None) self._data = data def get_data(self): @@ -114,4 +144,4 @@ class InlineImage(Message): class InfoMessage(TextMessage): def __init__(self, id, message, time): - super(InfoMessage, self).__init__(id, message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) + super().__init__(id, message, None, time, MESSAGE_TYPE['INFO_MESSAGE']) diff --git a/toxygen/middleware/threads.py b/toxygen/middleware/threads.py index 2d7bdf4..5d722f0 100644 --- a/toxygen/middleware/threads.py +++ b/toxygen/middleware/threads.py @@ -18,13 +18,13 @@ class BaseThread(threading.Thread): class InitThread(BaseThread): - def __init__(self, tox, plugin_loader): + def __init__(self, tox, plugin_loader, settings): super().__init__() - self._tox, self._plugin_loader = tox, plugin_loader + self._tox, self._plugin_loader, self._settings = tox, plugin_loader, settings def run(self): # download list of nodes if needed - download_nodes_list() + download_nodes_list(self._settings) # start plugins self._plugin_loader.load() # bootstrap diff --git a/toxygen/smileys/smileys.py b/toxygen/smileys/smileys.py index abf6990..c20d1a7 100644 --- a/toxygen/smileys/smileys.py +++ b/toxygen/smileys/smileys.py @@ -25,7 +25,7 @@ class SmileyLoader: pack_name = self._settings['smiley_pack'] if self._settings['smileys'] and self._curr_pack != pack_name: self._curr_pack = pack_name - path = self.get_smileys_path() + 'config.json' + path = util.join_path(self.get_smileys_path(), 'config.json') try: with open(path, encoding='utf8') as fl: self._smileys = json.loads(fl.read()) @@ -45,7 +45,7 @@ class SmileyLoader: print('Smiley pack {} was not loaded. Error: {}'.format(pack_name, ex)) def get_smileys_path(self): - return util.curr_directory() + '/smileys/' + self._curr_pack + '/' if self._curr_pack is not None else None + return util.join_path(util.get_smileys_directory(), self._curr_pack) if self._curr_pack is not None else None @staticmethod def get_packs_list(self): diff --git a/toxygen/ui/items_factory.py b/toxygen/ui/items_factory.py index cdf127a..386e762 100644 --- a/toxygen/ui/items_factory.py +++ b/toxygen/ui/items_factory.py @@ -3,9 +3,9 @@ from ui.list_items import * class ItemsFactory: - def __init__(self, friends_list, messages): - self._friends = friends_list - self._messages = messages + def __init__(self, settings, plugin_loader, smiley_loader, main_screen): + self._settings, self._plugin_loader = settings, plugin_loader + self._smiley_loader, self._main_screen = smiley_loader, main_screen def friend_item(self): item = ContactItem() diff --git a/toxygen/ui/list_items.py b/toxygen/ui/list_items.py index 76e6185..1891d45 100644 --- a/toxygen/ui/list_items.py +++ b/toxygen/ui/list_items.py @@ -10,208 +10,6 @@ from user_data import settings import re -class MessageEdit(QtWidgets.QTextBrowser): - - def __init__(self, text, width, message_type, parent=None): - super(MessageEdit, self).__init__(parent) - self.urls = {} - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere) - self.document().setTextWidth(width) - self.setOpenExternalLinks(True) - self.setAcceptRichText(True) - self.setOpenLinks(False) - path = smileys.SmileyLoader.get_instance().get_smileys_path() - if path is not None: - self.setSearchPaths([path]) - self.document().setDefaultStyleSheet('a { color: #306EFF; }') - text = self.decoratedText(text) - if message_type != TOX_MESSAGE_TYPE['NORMAL']: - self.setHtml('

' + text + '

') - else: - self.setHtml(text) - font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPixelSize(settings.Settings.get_instance()['message_font_size']) - font.setBold(False) - self.setFont(font) - self.resize(width, self.document().size().height()) - self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) - self.anchorClicked.connect(self.on_anchor_clicked) - - def contextMenuEvent(self, event): - menu = create_menu(self.createStandardContextMenu(event.pos())) - quote = menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Quote selected text')) - quote.triggered.connect(self.quote_text) - text = self.textCursor().selection().toPlainText() - if not text: - quote.setEnabled(False) - else: - import plugin_support - submenu = plugin_support.PluginLoader.get_instance().get_message_menu(menu, text) - if len(submenu): - plug = menu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins')) - plug.addActions(submenu) - menu.popup(event.globalPos()) - menu.exec_(event.globalPos()) - del menu - - def quote_text(self): - text = self.textCursor().selection().toPlainText() - if text: - from ui import main_screen - window = main_screen.MainWindow.get_instance() - text = '>' + '\n>'.join(text.split('\n')) - if window.messageEdit.toPlainText(): - text = '\n' + text - window.messageEdit.appendPlainText(text) - - def on_anchor_clicked(self, url): - text = str(url.toString()) - if text.startswith('tox:'): - from ui import menu - self.add_contact = menu.AddContact(text[4:]) - self.add_contact.show() - else: - QtGui.QDesktopServices.openUrl(url) - self.clearFocus() - - def addAnimation(self, url, fileName): - movie = QtGui.QMovie(self) - movie.setFileName(fileName) - self.urls[movie] = url - movie.frameChanged[int].connect(lambda x: self.animate(movie)) - movie.start() - - def animate(self, movie): - self.document().addResource(QtGui.QTextDocument.ImageResource, - self.urls[movie], - movie.currentPixmap()) - self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth()) - - def decoratedText(self, text): - text = h.escape(text) # replace < and > - exp = QtCore.QRegExp( - '(' - '(?:\\b)((www\\.)|(http[s]?|ftp)://)' - '\\w+\\S+)' - '|(?:\\b)(file:///)([\\S| ]*)' - '|(?:\\b)(tox:[a-zA-Z\\d]{76}$)' - '|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)' - '|(?:\\b)(tox:\\S+@\\S+)') - offset = exp.indexIn(text, 0) - while offset != -1: # add links - url = exp.cap() - if exp.cap(2) == 'www.': - html = '{0}'.format(url) - else: - html = '{0}'.format(url) - text = text[:offset] + html + text[offset + len(exp.cap()):] - offset += len(html) - offset = exp.indexIn(text, offset) - arr = text.split('\n') - for i in range(len(arr)): # quotes - if arr[i].startswith('>'): - arr[i] = '' + arr[i][4:] + '' - text = '
'.join(arr) - text = smileys.SmileyLoader.get_instance().add_smileys_to_text(text, self) # smileys - return text - - -class MessageItem(QtWidgets.QWidget): - """ - Message in messages list - """ - def __init__(self, text, time, user='', sent=True, message_type=TOX_MESSAGE_TYPE['NORMAL'], parent=None): - QtWidgets.QWidget.__init__(self, parent) - self.name = DataLabel(self) - self.name.setGeometry(QtCore.QRect(2, 2, 95, 23)) - self.name.setTextFormat(QtCore.Qt.PlainText) - font = QtGui.QFont() - font.setFamily(settings.Settings.get_instance()['font']) - font.setPointSize(11) - font.setBold(True) - self.name.setFont(font) - self.name.setText(user) - - self.time = QtWidgets.QLabel(self) - self.time.setGeometry(QtCore.QRect(parent.width() - 60, 0, 50, 25)) - font.setPointSize(10) - font.setBold(False) - self.time.setFont(font) - self._time = time - if not sent: - movie = QtGui.QMovie(curr_directory() + '/images/spinner.gif') - self.time.setMovie(movie) - movie.start() - self.t = True - else: - self.time.setText(convert_time(time)) - self.t = False - - self.message = MessageEdit(text, parent.width() - 160, message_type, self) - if message_type != TOX_MESSAGE_TYPE['NORMAL']: - self.name.setStyleSheet("QLabel { color: #5CB3FF; }") - self.message.setAlignment(QtCore.Qt.AlignCenter) - self.time.setStyleSheet("QLabel { color: #5CB3FF; }") - self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 160, self.message.height())) - self.setFixedHeight(self.message.height()) - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x(): - self.listMenu = QtWidgets.QMenu() - delete_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Delete message')) - delete_item.triggered.connect(self.delete) - parent_position = self.time.mapToGlobal(QtCore.QPoint(0, 0)) - self.listMenu.move(parent_position) - self.listMenu.show() - - def delete(self): - pr = profile.Profile.get_instance() - pr.delete_message(self._time) - - def mark_as_sent(self): - if self.t: - self.time.setText(convert_time(self._time)) - self.t = False - return True - return False - - def set_avatar(self, pixmap): - self.name.setAlignment(QtCore.Qt.AlignCenter) - self.message.setAlignment(QtCore.Qt.AlignVCenter) - self.setFixedHeight(max(self.height(), 36)) - self.name.setFixedHeight(self.height()) - self.message.setFixedHeight(self.height()) - self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) - - def select_text(self, text): - tmp = self.message.toHtml() - text = h.escape(text) - strings = re.findall(text, tmp, flags=re.IGNORECASE) - for s in strings: - tmp = self.replace_all(tmp, s) - self.message.setHtml(tmp) - - @staticmethod - def replace_all(text, substring): - i, l = 0, len(substring) - while i < len(text) - l + 1: - index = text[i:].find(substring) - if index == -1: - break - i += index - lgt, rgt = text[i:].find('<'), text[i:].find('>') - if rgt < lgt: - i += rgt + 1 - continue - sub = '{}'.format(substring) - text = text[:i] + sub + text[i + l:] - i += len(sub) - return text - - class ContactItem(QtWidgets.QWidget): """ Contact in friends list diff --git a/toxygen/ui/main_screen.py b/toxygen/ui/main_screen.py index 223e6dd..3852867 100644 --- a/toxygen/ui/main_screen.py +++ b/toxygen/ui/main_screen.py @@ -21,6 +21,7 @@ class MainWindow(QtWidgets.QMainWindow): self._saved = False if settings['show_welcome_screen']: self.ws = WelcomeScreen() + self.profile = None def setup_menu(self, window): self.menubar = QtWidgets.QMenuBar(window) @@ -108,42 +109,42 @@ class MainWindow(QtWidgets.QMainWindow): if event.type() == QtCore.QEvent.WindowActivate: self.tray.setIcon(QtGui.QIcon(curr_directory() + '/images/icon.png')) self.messages.repaint() - return super(MainWindow, self).event(event) + return super().event(event) def retranslateUi(self): - self.lockApp.setText(QtWidgets.QApplication.translate("MainWindow", "Lock")) - self.menuPlugins.setTitle(QtWidgets.QApplication.translate("MainWindow", "Plugins")) - self.pluginData.setText(QtWidgets.QApplication.translate("MainWindow", "List of plugins")) - self.menuProfile.setTitle(QtWidgets.QApplication.translate("MainWindow", "Profile")) - self.menuSettings.setTitle(QtWidgets.QApplication.translate("MainWindow", "Settings")) - self.menuAbout.setTitle(QtWidgets.QApplication.translate("MainWindow", "About")) - self.actionAdd_friend.setText(QtWidgets.QApplication.translate("MainWindow", "Add contact")) - self.actionAdd_gc.setText(QtWidgets.QApplication.translate("MainWindow", "Create group chat")) - self.actionprofilesettings.setText(QtWidgets.QApplication.translate("MainWindow", "Profile")) - self.actionPrivacy_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Privacy")) - self.actionInterface_settings.setText(QtWidgets.QApplication.translate("MainWindow", "Interface")) - self.actionNotifications.setText(QtWidgets.QApplication.translate("MainWindow", "Notifications")) - self.actionNetwork.setText(QtWidgets.QApplication.translate("MainWindow", "Network")) - self.actionAbout_program.setText(QtWidgets.QApplication.translate("MainWindow", "About program")) - self.actionSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Settings")) - self.audioSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Audio")) - self.videoSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Video")) - self.updateSettings.setText(QtWidgets.QApplication.translate("MainWindow", "Updates")) - self.contact_name.setPlaceholderText(QtWidgets.QApplication.translate("MainWindow", "Search")) - self.sendMessageButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Send message")) - self.callButton.setToolTip(QtWidgets.QApplication.translate("MainWindow", "Start audio call with friend")) + self.lockApp.setText(util_ui.tr("Lock")) + self.menuPlugins.setTitle(util_ui.tr("Plugins")) + self.pluginData.setText(util_ui.tr("List of plugins")) + self.menuProfile.setTitle(util_ui.tr("Profile")) + self.menuSettings.setTitle(util_ui.tr("Settings")) + self.menuAbout.setTitle(util_ui.tr("About")) + self.actionAdd_friend.setText(util_ui.tr("Add contact")) + self.actionAdd_gc.setText(util_ui.tr("Create group chat")) + self.actionprofilesettings.setText(util_ui.tr("Profile")) + self.actionPrivacy_settings.setText(util_ui.tr("Privacy")) + self.actionInterface_settings.setText(util_ui.tr("Interface")) + self.actionNotifications.setText(util_ui.tr("Notifications")) + self.actionNetwork.setText(util_ui.tr("Network")) + self.actionAbout_program.setText(util_ui.tr("About program")) + self.actionSettings.setText(util_ui.tr("Settings")) + self.audioSettings.setText(util_ui.tr("Audio")) + self.videoSettings.setText(util_ui.tr("Video")) + self.updateSettings.setText(util_ui.tr("Updates")) + self.contact_name.setPlaceholderText(util_ui.tr("Search")) + self.sendMessageButton.setToolTip(util_ui.tr("Send message")) + self.callButton.setToolTip(util_ui.tr("Start audio call with friend")) self.online_contacts.clear() - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "All")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online first")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Name")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online and by name")) - self.online_contacts.addItem(QtWidgets.QApplication.translate("MainWindow", "Online first and by name")) + self.online_contacts.addItem(util_ui.tr("All")) + self.online_contacts.addItem(util_ui.tr("Online")) + self.online_contacts.addItem(util_ui.tr("Online first")) + self.online_contacts.addItem(util_ui.tr("Name")) + self.online_contacts.addItem(util_ui.tr("Online and by name")) + self.online_contacts.addItem(util_ui.tr("Online first and by name")) ind = self._settings['sorting'] d = {0: 0, 1: 1, 2: 2, 3: 4, 1 | 4: 4, 2 | 4: 5} self.online_contacts.setCurrentIndex(d[ind]) - self.importPlugin.setText(QtWidgets.QApplication.translate("MainWindow", "Import plugin")) - self.reloadPlugins.setText(QtWidgets.QApplication.translate("MainWindow", "Reload plugins")) + self.importPlugin.setText(util_ui.tr("Import plugin")) + self.reloadPlugins.setText(util_ui.tr("Reload plugins")) def setup_right_bottom(self, Form): Form.resize(650, 60) @@ -353,28 +354,27 @@ class MainWindow(QtWidgets.QMainWindow): self.user_info = name self.friend_info = info self.retranslateUi() - self.profile = Profile(tox, self) def closeEvent(self, event): - s = Settings.get_instance() - if not s['close_to_tray'] or s.closing: - if not self._saved: - self._saved = True - self.profile.save_history() - self.profile.close() - s['x'] = self.geometry().x() - s['y'] = self.geometry().y() - s['width'] = self.width() - s['height'] = self.height() - s.save() - QtWidgets.QApplication.closeAllWindows() - event.accept() + if not self._settings['close_to_tray'] or self._settings.closing: + if self._saved: + return + self._saved = True + self.profile.save_history() + self.profile.close() + self._settings['x'] = self.geometry().x() + self._settings['y'] = self.geometry().y() + self._settings['width'] = self.width() + self._settings['height'] = self.height() + self._settings.save() + QtWidgets.QApplication.closeAllWindows() + event.accept() elif QtWidgets.QSystemTrayIcon.isSystemTrayAvailable(): event.ignore() self.hide() def close_window(self): - Settings.get_instance().closing = True + self._settings.closing = True self.close() def resizeEvent(self, *args, **kwargs): @@ -471,7 +471,7 @@ class MainWindow(QtWidgets.QMainWindow): def import_plugin(self): import util directory = QtWidgets.QFileDialog.getExistingDirectory(self, - QtWidgets.QApplication.translate("MainWindow", 'Choose folder with plugin'), + util_ui.tr('Choose folder with plugin'), util.curr_directory(), QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) if directory: @@ -480,9 +480,9 @@ class MainWindow(QtWidgets.QMainWindow): util.copy(src, dest) msgBox = QtWidgets.QMessageBox() msgBox.setWindowTitle( - QtWidgets.QApplication.translate("MainWindow", "Restart Toxygen")) + util_ui.tr("Restart Toxygen")) msgBox.setText( - QtWidgets.QApplication.translate("MainWindow", 'Plugin will be loaded after restart')) + util_ui.tr('Plugin will be loaded after restart')) msgBox.exec_() def lock_app(self): @@ -492,9 +492,9 @@ class MainWindow(QtWidgets.QMainWindow): else: msgBox = QtWidgets.QMessageBox() msgBox.setWindowTitle( - QtWidgets.QApplication.translate("MainWindow", "Cannot lock app")) + util_ui.tr("Cannot lock app")) msgBox.setText( - QtWidgets.QApplication.translate("MainWindow", 'Error. Profile password is not set.')) + util_ui.tr('Error. Profile password is not set.')) msgBox.exec_() def show_menu(self): @@ -517,7 +517,7 @@ class MainWindow(QtWidgets.QMainWindow): def send_file(self): self.menu.hide() if self.profile.active_friend + 1and self.profile.is_active_a_friend(): - choose = QtWidgets.QApplication.translate("MainWindow", 'Choose file') + choose = util_ui.tr('Choose file') name = QtWidgets.QFileDialog.getOpenFileName(self, choose, options=QtWidgets.QFileDialog.DontUseNativeDialog) if name[0]: self.profile.send_file(name[0]) @@ -583,33 +583,33 @@ class MainWindow(QtWidgets.QMainWindow): return settings = Settings.get_instance() allowed = friend.tox_id in settings['auto_accept_from_friends'] - auto = QtWidgets.QApplication.translate("MainWindow", 'Disallow auto accept') if allowed else QtWidgets.QApplication.translate("MainWindow", 'Allow auto accept') + auto = util_ui.tr('Disallow auto accept') if allowed else util_ui.tr('Allow auto accept') if item is not None: self.listMenu = QtWidgets.QMenu() is_friend = type(friend) is Friend if is_friend: - set_alias_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set alias')) + set_alias_item = self.listMenu.addAction(util_ui.tr('Set alias')) set_alias_item.triggered.connect(lambda: self.set_alias(num)) - history_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Chat history')) - clear_history_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Clear history')) - export_to_text_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Export as text')) - export_to_html_item = history_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Export as HTML')) + history_menu = self.listMenu.addMenu(util_ui.tr('Chat history')) + clear_history_item = history_menu.addAction(util_ui.tr('Clear history')) + export_to_text_item = history_menu.addAction(util_ui.tr('Export as text')) + export_to_html_item = history_menu.addAction(util_ui.tr('Export as HTML')) - copy_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Copy')) - copy_name_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Name')) - copy_status_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Status message')) + copy_menu = self.listMenu.addMenu(util_ui.tr('Copy')) + copy_name_item = copy_menu.addAction(util_ui.tr('Name')) + copy_status_item = copy_menu.addAction(util_ui.tr('Status message')) if is_friend: - copy_key_item = copy_menu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Public key')) + copy_key_item = copy_menu.addAction(util_ui.tr('Public key')) auto_accept_item = self.listMenu.addAction(auto) - remove_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Remove friend')) - block_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Block friend')) - notes_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Notes')) + remove_item = self.listMenu.addAction(util_ui.tr('Remove friend')) + block_item = self.listMenu.addAction(util_ui.tr('Block friend')) + notes_item = self.listMenu.addAction(util_ui.tr('Notes')) chats = self.profile.get_group_chats() if len(chats) and self.profile.is_active_online(): - invite_menu = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Invite to group chat')) + invite_menu = self.listMenu.addMenu(util_ui.tr('Invite to group chat')) for i in range(len(chats)): name, number = chats[i] item = invite_menu.addAction(name) @@ -619,7 +619,7 @@ class MainWindow(QtWidgets.QMainWindow): if plugins_loader is not None: submenu = plugins_loader.get_menu(self.listMenu, num) if len(submenu): - plug = self.listMenu.addMenu(QtWidgets.QApplication.translate("MainWindow", 'Plugins')) + plug = self.listMenu.addMenu(util_ui.tr('Plugins')) plug.addActions(submenu) copy_key_item.triggered.connect(lambda: self.copy_friend_key(num)) remove_item.triggered.connect(lambda: self.remove_friend(num)) @@ -627,8 +627,8 @@ class MainWindow(QtWidgets.QMainWindow): auto_accept_item.triggered.connect(lambda: self.auto_accept(num, not allowed)) notes_item.triggered.connect(lambda: self.show_note(friend)) else: - leave_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Leave chat')) - set_title_item = self.listMenu.addAction(QtWidgets.QApplication.translate("MainWindow", 'Set title')) + leave_item = self.listMenu.addAction(util_ui.tr('Leave chat')) + set_title_item = self.listMenu.addAction(util_ui.tr('Set title')) leave_item.triggered.connect(lambda: self.leave_gc(num)) set_title_item.triggered.connect(lambda: self.set_title(num)) clear_history_item.triggered.connect(lambda: self.clear_history(num)) @@ -643,7 +643,7 @@ class MainWindow(QtWidgets.QMainWindow): def show_note(self, friend): s = Settings.get_instance() note = s['notes'][friend.tox_id] if friend.tox_id in s['notes'] else '' - user = QtWidgets.QApplication.translate("MainWindow", 'Notes about user') + user = util_ui.tr('Notes about user') user = '{} {}'.format(user, friend.name) def save_note(text): diff --git a/toxygen/ui/menu.py b/toxygen/ui/menu.py index 1aa12dc..9dcf180 100644 --- a/toxygen/ui/menu.py +++ b/toxygen/ui/menu.py @@ -16,6 +16,7 @@ class AddContact(CenteredWidget): super(AddContact, self).__init__() self.initUI(tox_id) self._adding = False + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) def initUI(self, tox_id): self.setObjectName('AddContact') diff --git a/toxygen/ui/messages_widgets.py b/toxygen/ui/messages_widgets.py new file mode 100644 index 0000000..ae21f8a --- /dev/null +++ b/toxygen/ui/messages_widgets.py @@ -0,0 +1,212 @@ +from PyQt5 import QtWidgets, QtGui, QtCore +from wrapper.toxcore_enums_and_consts import * +import ui.widgets as widgets +import util.ui as util_ui +import util.util as util +import ui.menu as menu +import html as h +import re + + +class MessageEdit(QtWidgets.QTextBrowser): + + def __init__(self, settings, message_edit, smileys_loader, plugin_loader, text, width, message_type, parent=None): + super().__init__(parent) + self.urls = {} + self._message_edit = message_edit + self._smileys_loader = smileys_loader + self._plugin_loader = plugin_loader + self._add_contact = None + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setWordWrapMode(QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere) + self.document().setTextWidth(width) + self.setOpenExternalLinks(True) + self.setAcceptRichText(True) + self.setOpenLinks(False) + path = smileys_loader.get_smileys_path() + if path is not None: + self.setSearchPaths([path]) + self.document().setDefaultStyleSheet('a { color: #306EFF; }') + text = self.decoratedText(text) + if message_type != TOX_MESSAGE_TYPE['NORMAL']: + self.setHtml('

' + text + '

') + else: + self.setHtml(text) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPixelSize(settings['message_font_size']) + font.setBold(False) + self.setFont(font) + self.resize(width, self.document().size().height()) + self.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse) + self.anchorClicked.connect(self.on_anchor_clicked) + + def contextMenuEvent(self, event): + menu = widgets.create_menu(self.createStandardContextMenu(event.pos())) + quote = menu.addAction(util_ui.tr('Quote selected text')) + quote.triggered.connect(self.quote_text) + text = self.textCursor().selection().toPlainText() + if not text: + quote.setEnabled(False) + else: + sub_menu = self._plugin_loader.get_message_menu(menu, text) + if len(sub_menu): + plugins_menu = menu.addMenu(util_ui.tr('Plugins')) + plugins_menu.addActions(sub_menu) + menu.popup(event.globalPos()) + menu.exec_(event.globalPos()) + del menu + + def quote_text(self): + text = self.textCursor().selection().toPlainText() + if not text: + return + text = '>' + '\n>'.join(text.split('\n')) + if self._message_edit.toPlainText(): + text = '\n' + text + self._message_edit.appendPlainText(text) + + def on_anchor_clicked(self, url): + text = str(url.toString()) + if text.startswith('tox:'): + self._add_contact = menu.AddContact(text[4:]) + self._add_contact.show() + else: + QtGui.QDesktopServices.openUrl(url) + self.clearFocus() + + def addAnimation(self, url, file_name): + movie = QtGui.QMovie(self) + movie.setFileName(file_name) + self.urls[movie] = url + movie.frameChanged[int].connect(lambda x: self.animate(movie)) + movie.start() + + def animate(self, movie): + self.document().addResource(QtGui.QTextDocument.ImageResource, + self.urls[movie], + movie.currentPixmap()) + self.setLineWrapColumnOrWidth(self.lineWrapColumnOrWidth()) + + def decoratedText(self, text): + text = h.escape(text) # replace < and > + exp = QtCore.QRegExp( + '(' + '(?:\\b)((www\\.)|(http[s]?|ftp)://)' + '\\w+\\S+)' + '|(?:\\b)(file:///)([\\S| ]*)' + '|(?:\\b)(tox:[a-zA-Z\\d]{76}$)' + '|(?:\\b)(mailto:\\S+@\\S+\\.\\S+)' + '|(?:\\b)(tox:\\S+@\\S+)') + offset = exp.indexIn(text, 0) + while offset != -1: # add links + url = exp.cap() + if exp.cap(2) == 'www.': + html = '{0}'.format(url) + else: + html = '{0}'.format(url) + text = text[:offset] + html + text[offset + len(exp.cap()):] + offset += len(html) + offset = exp.indexIn(text, offset) + arr = text.split('\n') + for i in range(len(arr)): # quotes + if arr[i].startswith('>'): + arr[i] = '' + arr[i][4:] + '' + text = '
'.join(arr) + text = self._smileys_loader.add_smileys_to_text(text, self) # smileys + return text + + +class MessageItem(QtWidgets.QWidget): + """ + Message in messages list + """ + def __init__(self, settings, message_edit_factory, text_message, parent=None): + QtWidgets.QWidget.__init__(self, parent) + self.name = widgets.DataLabel(self) + self.name.setGeometry(QtCore.QRect(2, 2, 95, 23)) + self.name.setTextFormat(QtCore.Qt.PlainText) + font = QtGui.QFont() + font.setFamily(settings['font']) + font.setPointSize(11) + font.setBold(True) + self.name.setFont(font) + self.name.setText(text_message.user) + + self.time = QtWidgets.QLabel(self) + self.time.setGeometry(QtCore.QRect(parent.width() - 60, 0, 50, 25)) + font.setPointSize(10) + font.setBold(False) + self.time.setFont(font) + self._time = time + if not sent: + movie = QtGui.QMovie(util.join_path(util.get_images_directory(), 'spinner.gif')) + self.time.setMovie(movie) + movie.start() + self.t = True + else: + self.time.setText(util.convert_time(time)) + self.t = False + + self.message = MessageEdit(text, parent.width() - 160, message_type, self) + if message_type != TOX_MESSAGE_TYPE['NORMAL']: + self.name.setStyleSheet("QLabel { color: #5CB3FF; }") + self.message.setAlignment(QtCore.Qt.AlignCenter) + self.time.setStyleSheet("QLabel { color: #5CB3FF; }") + self.message.setGeometry(QtCore.QRect(100, 0, parent.width() - 160, self.message.height())) + self.setFixedHeight(self.message.height()) + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton and event.x() > self.time.x(): + self.listMenu = QtWidgets.QMenu() + delete_item = self.listMenu.addAction(util_ui.tr('Delete message')) + delete_item.triggered.connect(self.delete) + parent_position = self.time.mapToGlobal(QtCore.QPoint(0, 0)) + self.listMenu.move(parent_position) + self.listMenu.show() + + def delete(self): + pr = profile.Profile.get_instance() + pr.delete_message(self._time) + + def mark_as_sent(self): + if self.t: + self.time.setText(convert_time(self._time)) + self.t = False + return True + return False + + def set_avatar(self, pixmap): + self.name.setAlignment(QtCore.Qt.AlignCenter) + self.message.setAlignment(QtCore.Qt.AlignVCenter) + self.setFixedHeight(max(self.height(), 36)) + self.name.setFixedHeight(self.height()) + self.message.setFixedHeight(self.height()) + self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) + + def select_text(self, text): + tmp = self.message.toHtml() + text = h.escape(text) + strings = re.findall(text, tmp, flags=re.IGNORECASE) + for s in strings: + tmp = self.replace_all(tmp, s) + self.message.setHtml(tmp) + + @staticmethod + def replace_all(text, substring): + i, l = 0, len(substring) + while i < len(text) - l + 1: + index = text[i:].find(substring) + if index == -1: + break + i += index + lgt, rgt = text[i:].find('<'), text[i:].find('>') + if rgt < lgt: + i += rgt + 1 + continue + sub = '{}'.format(substring) + text = text[:i] + sub + text[i + l:] + i += len(sub) + return text + diff --git a/toxygen/ui/widgets.py b/toxygen/ui/widgets.py index 7b811e7..aab027a 100644 --- a/toxygen/ui/widgets.py +++ b/toxygen/ui/widgets.py @@ -66,14 +66,14 @@ class QRightClickButton(QtWidgets.QPushButton): rightClicked = QtCore.pyqtSignal() - def __init__(self, parent): - super(QRightClickButton, self).__init__(parent) + def __init__(self, parent=None): + super().__init__(parent) def mousePressEvent(self, event): if event.button() == QtCore.Qt.RightButton: self.rightClicked.emit() else: - super(QRightClickButton, self).mousePressEvent(event) + super().mousePressEvent(event) class RubberBand(QtWidgets.QRubberBand): diff --git a/toxygen/user_data/profile_manager.py b/toxygen/user_data/profile_manager.py index 823dbc0..6107d43 100644 --- a/toxygen/user_data/profile_manager.py +++ b/toxygen/user_data/profile_manager.py @@ -11,11 +11,11 @@ class ProfileManager: self._settings = settings self._toxes = toxes self._path = path - self._directory = os.path.basename(path) + self._directory = os.path.dirname(path) # create /avatars if not exists: - directory = util.join_path(self._directory, 'avatars') - if not os.path.exists(directory): - os.makedirs(directory) + avatars_directory = util.join_path(self._directory, 'avatars') + if not os.path.exists(avatars_directory): + os.makedirs(avatars_directory) def open_profile(self): with open(self._path, 'rb') as fl: diff --git a/toxygen/util/util.py b/toxygen/util/util.py index 61df479..b93a45a 100644 --- a/toxygen/util/util.py +++ b/toxygen/util/util.py @@ -55,6 +55,11 @@ def get_stickers_directory(): return get_app_directory('stickers') +@cached +def get_smileys_directory(): + return get_app_directory('smileys') + + @cached def get_translations_directory(): return get_app_directory('translations') @@ -143,3 +148,12 @@ def is_re_valid(regex): def get_platform(): return platform.system() + + +class ToxSave: + + def __init__(self, tox): + self._tox = tox + + def set_tox(self, tox): + self._tox = tox