diff --git a/toxygen/contact.py b/toxygen/contact.py index 28c6442..41628c3 100644 --- a/toxygen/contact.py +++ b/toxygen/contact.py @@ -30,7 +30,8 @@ class Contact(basecontact.BaseContact): self._unsaved_messages = 0 self._history_loaded = self._new_actions = False self._receipts = 0 - self._curr_text = '' + self._curr_text = self._search_string = '' + self._search_index = 0 def __del__(self): self.set_visibility(False) @@ -94,6 +95,10 @@ class Contact(basecontact.BaseContact): else: return '' + # ----------------------------------------------------------------------------------------------------------------- + # Unsent messages + # ----------------------------------------------------------------------------------------------------------------- + def get_unsent_messages(self): """ :return list of unsent messages @@ -108,6 +113,17 @@ class Contact(basecontact.BaseContact): messages = filter(lambda x: x.get_type() <= 1 and x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr) return list(map(lambda x: x.get_data(), messages)) + def mark_as_sent(self): + try: + message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] + message.mark_as_sent() + except Exception as ex: + util.log('Mark as sent ex: ' + str(ex)) + + # ----------------------------------------------------------------------------------------------------------------- + # Message deletion + # ----------------------------------------------------------------------------------------------------------------- + def delete_message(self, time): elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0] tmp = list(filter(lambda x: x.get_type() <= 1, self._corr)) @@ -118,7 +134,7 @@ class Contact(basecontact.BaseContact): def delete_old_messages(self): """ - Delete old messages (reduces RAM if messages saving is not enabled) + Delete old messages (reduces RAM usage if messages saving is not enabled) """ old = filter(lambda x: x.get_type() == 2 and (x.get_status() >= 2 or x.get_status() is None), self._corr[:-SAVE_MESSAGES]) @@ -126,13 +142,7 @@ class Contact(basecontact.BaseContact): l = max(len(self._corr) - SAVE_MESSAGES, 0) - len(old) self._unsaved_messages -= l self._corr = old + self._corr[-SAVE_MESSAGES:] - - def mark_as_sent(self): - try: - message = list(filter(lambda x: x.get_owner() == MESSAGE_OWNER['NOT_SENT'], self._corr))[0] - message.mark_as_sent() - except Exception as ex: - util.log('Mark as sent ex: ' + str(ex)) + self._search_index = 0 def clear_corr(self, save_unsent=False): """ @@ -140,6 +150,7 @@ class Contact(basecontact.BaseContact): """ if hasattr(self, '_message_getter'): del self._message_getter + self._search_index = 0 # don't delete data about active file transfer if not save_unsent: self._corr = list(filter(lambda x: x.get_type() == 2 and @@ -151,6 +162,43 @@ class Contact(basecontact.BaseContact): self._corr)) self._unsaved_messages = len(self.get_unsent_messages()) + # ----------------------------------------------------------------------------------------------------------------- + # Chat history search + # ----------------------------------------------------------------------------------------------------------------- + + def search_string(self, search_string): + self._search_string, self._search_index = search_string, 0 + return self.search_prev() + + def search_prev(self): + while True: + l = len(self._corr) + for i in range(self._search_index - 1, -l - 1, -1): + if type(self._corr[i]) is not TextMessage: + continue + if self._search_string.lower() in self._corr[i].get_data()[0].lower(): + self._search_index = i + return i + self._search_index = -l + self.load_corr(False) + if len(self._corr) == l: + return None # not found + + def search_next(self): + if not self._search_index: + return None + for i in range(self._search_index + 1, 0): + if type(self._corr[i]) is not TextMessage: + continue + if self._search_string.lower() in self._corr[i].get_data()[0].lower(): + self._search_index = i + return i + return None # not found + + # ----------------------------------------------------------------------------------------------------------------- + # Current text - text from message area + # ----------------------------------------------------------------------------------------------------------------- + def get_curr_text(self): return self._curr_text diff --git a/toxygen/list_items.py b/toxygen/list_items.py index b782231..9b84e13 100644 --- a/toxygen/list_items.py +++ b/toxygen/list_items.py @@ -11,6 +11,7 @@ from widgets import DataLabel, create_menu import html as h import smileys import settings +import re class MessageEdit(QtGui.QTextBrowser): @@ -189,6 +190,12 @@ class MessageItem(QtGui.QWidget): 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() + pattern = re.compile(re.escape(text), re.IGNORECASE) # TODO: save case + tmp = pattern.sub('{}'.format(text), tmp) + self.message.setHtml(tmp) + class ContactItem(QtGui.QWidget): """ diff --git a/toxygen/mainscreen.py b/toxygen/mainscreen.py index 90cea71..04a0318 100644 --- a/toxygen/mainscreen.py +++ b/toxygen/mainscreen.py @@ -407,6 +407,8 @@ class MainWindow(QtGui.QMainWindow, Singleton): clipboard.setText(s) elif event.key() == QtCore.Qt.Key_Z and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): self.messages.clearSelection() + elif event.key() == QtCore.Qt.Key_F and event.modifiers() & QtCore.Qt.ControlModifier: + self.show_search_field() else: super(MainWindow, self).keyPressEvent(event) @@ -699,3 +701,12 @@ class MainWindow(QtGui.QMainWindow, Singleton): ind = self.online_contacts.currentIndex() d = {0: 0, 1: 1, 2: 2, 3: 4, 4: 1 | 4, 5: 2 | 4} self.profile.filtration_and_sorting(d[ind], self.contact_name.text()) + + def show_search_field(self): + if hasattr(self, 'search_field') and self.search_field.isVisible(): + return + self.search_field = SearchScreen(self.messages, self.messages.width(), self.messages.parent()) + x, y = self.messages.x(), self.messages.y() + self.messages.height() - 40 + self.search_field.setGeometry(x, y, self.messages.width(), 40) + self.messages.setGeometry(x, self.messages.y(), self.messages.width(), self.messages.height() - 40) + self.search_field.show() diff --git a/toxygen/mainscreen_widgets.py b/toxygen/mainscreen_widgets.py index 9bf654d..e277dc4 100644 --- a/toxygen/mainscreen_widgets.py +++ b/toxygen/mainscreen_widgets.py @@ -2,7 +2,7 @@ try: from PySide import QtCore, QtGui except ImportError: from PyQt4 import QtCore, QtGui -from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget +from widgets import RubberBand, create_menu, QRightClickButton, CenteredWidget, LineEdit from profile import Profile import smileys import util @@ -404,3 +404,96 @@ class MainMenuButton(QtGui.QPushButton): metrics = QtGui.QFontMetrics(self.font()) self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20) super().setText(text) + + +class ClickableLabel(QtGui.QLabel): + + def __init__(self, *args): + super().__init__(*args) + + def mouseReleaseEvent(self, ev): + self.emit(QtCore.SIGNAL('clicked()')) + + +class SearchScreen(QtGui.QWidget): + + def __init__(self, messages, width, *args): + super().__init__(*args) + self.setMaximumSize(width, 40) + self.setMinimumSize(width, 40) + self._messages = messages + + self.search_text = LineEdit(self) + self.search_text.setGeometry(0, 0, width - 100, 40) + + self.search_button = ClickableLabel(self) + self.search_button.setGeometry(width - 100, 0, 40, 40) + pixmap = QtGui.QPixmap() + pixmap.load(util.curr_directory() + '/images/search.png') + self.search_button.setScaledContents(False) + self.search_button.setAlignment(QtCore.Qt.AlignCenter) + self.search_button.setPixmap(pixmap) + self.connect(self.search_button, QtCore.SIGNAL('clicked()'), self.search) + + self.prev_button = QtGui.QPushButton(self) + self.prev_button.setGeometry(width - 60, 0, 20, 20) + self.prev_button.clicked.connect(self.prev) + self.prev_button.setText('\u25B2') + + self.next_button = QtGui.QPushButton(self) + self.next_button.setGeometry(width - 60, 20, 20, 20) + self.next_button.clicked.connect(self.next) + self.next_button.setText('\u25BC') + + self.close_button = QtGui.QPushButton(self) + self.close_button.setGeometry(width - 40, 0, 40, 40) + self.close_button.clicked.connect(self.close) + self.close_button.setText('×') + self.close_button.setAlignment(QtCore.Qt.AlignCenter) + font = QtGui.QFont() + font.setPointSize(32) + font.setBold(True) + self.close_button.setFont(font) + + def search(self): + text = self.search_text.text() # TODO: clean selection + friend = Profile.get_instance().get_curr_friend() + if text and friend: + index = friend.search_string(text) + self.load_messages(index) + + def prev(self): + friend = Profile.get_instance().get_curr_friend() + if friend is not None: + index = friend.search_prev() + self.load_messages(index) + + def next(self): + friend = Profile.get_instance().get_curr_friend() + if friend is not None: + index = friend.search_next() + if index is not None: + text = self.search_text.text() + count = self._messages.count() + index += count + item = self._messages.item(index) + self._messages.scrollToItem(item) + self._messages.itemWidget(item).select_text(text) + + def load_messages(self, index): + if index is not None: + profile = Profile.get_instance() + count = self._messages.count() + while count + index < 0: + profile.load_history() + count = self._messages.count() + index += count + item = self._messages.item(index) + self._messages.scrollToItem(item) + text = self.search_text.text() + self._messages.itemWidget(item).select_text(text) + + def closeEvent(self, *args): + Profile.get_instance().update() # TODO: clean selection? + self._messages.setGeometry(0, 0, self._messages.width(), self._messages.height() + 40) + super().closeEvent(*args) diff --git a/toxygen/profile.py b/toxygen/profile.py index 1bc3507..d8c6d3c 100644 --- a/toxygen/profile.py +++ b/toxygen/profile.py @@ -183,6 +183,9 @@ class Profile(basecontact.BaseContact, Singleton): return None return self._contacts[num] + def get_curr_friend(self): + return self._contacts[self._active_friend] if self._active_friend + 1 else None + # ----------------------------------------------------------------------------------------------------------------- # Work with active friend # ----------------------------------------------------------------------------------------------------------------- @@ -211,7 +214,7 @@ class Profile(basecontact.BaseContact, Singleton): if value is not None: if self._active_friend + 1 and self._active_friend != value: try: - self._contacts[self._active_friend].curr_text = self._screen.messageEdit.toPlainText() + self.get_curr_friend().curr_text = self._screen.messageEdit.toPlainText() except: pass friend = self._contacts[value] @@ -261,7 +264,7 @@ class Profile(basecontact.BaseContact, Singleton): else: self._screen.call_finished() else: - friend = self._contacts[self._active_friend] + friend = self.get_curr_friend() self._screen.account_name.setText(friend.name) self._screen.account_status.setText(friend.status_message) @@ -287,18 +290,18 @@ class Profile(basecontact.BaseContact, Singleton): def get_last_message(self): if self._active_friend + 1: - return self._contacts[self._active_friend].get_last_message_text() + return self.get_curr_friend().get_last_message_text() else: return '' def get_active_number(self): - return self._contacts[self._active_friend].number if self._active_friend + 1 else -1 + return self.get_curr_friend().number if self._active_friend + 1 else -1 def get_active_name(self): - return self._contacts[self._active_friend].name if self._active_friend + 1 else '' + return self.get_curr_friend().name if self._active_friend + 1 else '' def is_active_online(self): - return self._active_friend + 1 and self._contacts[self._active_friend].status is not None + return self._active_friend + 1 and self.get_curr_friend().status is not None def new_name(self, number, name): friend = self.get_friend_by_number(number) @@ -373,7 +376,7 @@ class Profile(basecontact.BaseContact, Singleton): """ if Settings.get_instance()['typing_notifications'] and self._active_friend + 1: try: - friend = self._contacts[self._active_friend] + friend = self.get_curr_friend() if friend.status is not None: self._tox.self_set_typing(friend.number, typing) except: @@ -443,7 +446,7 @@ class Profile(basecontact.BaseContact, Singleton): t = time.time() self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) self._messages.scrollToBottom() - self._contacts[self._active_friend].append_message( + self.get_curr_friend().append_message( TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type)) else: friend = self.get_friend_by_number(friend_num) @@ -482,7 +485,7 @@ class Profile(basecontact.BaseContact, Singleton): friend.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type)) def delete_message(self, time): - friend = self._contacts[self._active_friend] + friend = self.get_curr_friend() friend.delete_message(time) self._history.delete_message(friend.tox_id, time) self.update() @@ -536,7 +539,7 @@ class Profile(basecontact.BaseContact, Singleton): if not self._load_history: return self._load_history = False - friend = self._contacts[self._active_friend] + friend = self.get_curr_friend() friend.load_corr(False) data = friend.get_corr() if not data: @@ -624,7 +627,7 @@ class Profile(basecontact.BaseContact, Singleton): pixmap = None if self._show_avatars: if owner == MESSAGE_OWNER['FRIEND']: - pixmap = self._contacts[self._active_friend].get_pixmap() + pixmap = self.get_curr_friend().get_pixmap() else: pixmap = self.get_pixmap() return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], @@ -968,7 +971,7 @@ class Profile(basecontact.BaseContact, Singleton): 0, -1) def cancel_not_started_transfer(self, time): - self._contacts[self._active_friend].delete_one_unsent_file(time) + self.get_curr_friend().delete_one_unsent_file(time) self.update() def pause_transfer(self, friend_number, file_number, by_friend=False): @@ -1202,7 +1205,7 @@ class Profile(basecontact.BaseContact, Singleton): else: text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None, QtGui.QApplication.UnicodeUTF8) - self._contacts[self._active_friend].append_message(InfoMessage(text, time.time())) + self.get_curr_friend().append_message(InfoMessage(text, time.time())) self.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE']) self._messages.scrollToBottom() elif num in self._call: # finish or cancel call if you call with active friend