search in history by ctrl + F - initial commit

This commit is contained in:
ingvar1995 2017-02-12 17:58:23 +03:00
parent 1d33d298c3
commit 8b56184510
5 changed files with 185 additions and 23 deletions

View file

@ -30,7 +30,8 @@ class Contact(basecontact.BaseContact):
self._unsaved_messages = 0 self._unsaved_messages = 0
self._history_loaded = self._new_actions = False self._history_loaded = self._new_actions = False
self._receipts = 0 self._receipts = 0
self._curr_text = '' self._curr_text = self._search_string = ''
self._search_index = 0
def __del__(self): def __del__(self):
self.set_visibility(False) self.set_visibility(False)
@ -94,6 +95,10 @@ class Contact(basecontact.BaseContact):
else: else:
return '' return ''
# -----------------------------------------------------------------------------------------------------------------
# Unsent messages
# -----------------------------------------------------------------------------------------------------------------
def get_unsent_messages(self): def get_unsent_messages(self):
""" """
:return list of unsent messages :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) 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)) 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): def delete_message(self, time):
elem = list(filter(lambda x: type(x) is TextMessage and x.get_data()[2] == time, self._corr))[0] 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)) tmp = list(filter(lambda x: x.get_type() <= 1, self._corr))
@ -118,7 +134,7 @@ class Contact(basecontact.BaseContact):
def delete_old_messages(self): 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), old = filter(lambda x: x.get_type() == 2 and (x.get_status() >= 2 or x.get_status() is None),
self._corr[:-SAVE_MESSAGES]) self._corr[:-SAVE_MESSAGES])
@ -126,13 +142,7 @@ class Contact(basecontact.BaseContact):
l = max(len(self._corr) - SAVE_MESSAGES, 0) - len(old) l = max(len(self._corr) - SAVE_MESSAGES, 0) - len(old)
self._unsaved_messages -= l self._unsaved_messages -= l
self._corr = old + self._corr[-SAVE_MESSAGES:] self._corr = old + self._corr[-SAVE_MESSAGES:]
self._search_index = 0
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))
def clear_corr(self, save_unsent=False): def clear_corr(self, save_unsent=False):
""" """
@ -140,6 +150,7 @@ class Contact(basecontact.BaseContact):
""" """
if hasattr(self, '_message_getter'): if hasattr(self, '_message_getter'):
del self._message_getter del self._message_getter
self._search_index = 0
# don't delete data about active file transfer # don't delete data about active file transfer
if not save_unsent: if not save_unsent:
self._corr = list(filter(lambda x: x.get_type() == 2 and self._corr = list(filter(lambda x: x.get_type() == 2 and
@ -151,6 +162,43 @@ class Contact(basecontact.BaseContact):
self._corr)) self._corr))
self._unsaved_messages = len(self.get_unsent_messages()) 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): def get_curr_text(self):
return self._curr_text return self._curr_text

View file

@ -11,6 +11,7 @@ from widgets import DataLabel, create_menu
import html as h import html as h
import smileys import smileys
import settings import settings
import re
class MessageEdit(QtGui.QTextBrowser): class MessageEdit(QtGui.QTextBrowser):
@ -189,6 +190,12 @@ class MessageItem(QtGui.QWidget):
self.message.setFixedHeight(self.height()) self.message.setFixedHeight(self.height())
self.name.setPixmap(pixmap.scaled(30, 30, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)) 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('<font color="red">{}</font>'.format(text), tmp)
self.message.setHtml(tmp)
class ContactItem(QtGui.QWidget): class ContactItem(QtGui.QWidget):
""" """

View file

@ -407,6 +407,8 @@ class MainWindow(QtGui.QMainWindow, Singleton):
clipboard.setText(s) clipboard.setText(s)
elif event.key() == QtCore.Qt.Key_Z and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes(): elif event.key() == QtCore.Qt.Key_Z and event.modifiers() & QtCore.Qt.ControlModifier and self.messages.selectedIndexes():
self.messages.clearSelection() self.messages.clearSelection()
elif event.key() == QtCore.Qt.Key_F and event.modifiers() & QtCore.Qt.ControlModifier:
self.show_search_field()
else: else:
super(MainWindow, self).keyPressEvent(event) super(MainWindow, self).keyPressEvent(event)
@ -699,3 +701,12 @@ class MainWindow(QtGui.QMainWindow, Singleton):
ind = self.online_contacts.currentIndex() ind = self.online_contacts.currentIndex()
d = {0: 0, 1: 1, 2: 2, 3: 4, 4: 1 | 4, 5: 2 | 4} 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()) 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()

View file

@ -2,7 +2,7 @@ try:
from PySide import QtCore, QtGui from PySide import QtCore, QtGui
except ImportError: except ImportError:
from PyQt4 import QtCore, QtGui 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 from profile import Profile
import smileys import smileys
import util import util
@ -404,3 +404,96 @@ class MainMenuButton(QtGui.QPushButton):
metrics = QtGui.QFontMetrics(self.font()) metrics = QtGui.QFontMetrics(self.font())
self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20) self.setFixedWidth(metrics.size(QtCore.Qt.TextSingleLine, text).width() + 20)
super().setText(text) 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)

View file

@ -183,6 +183,9 @@ class Profile(basecontact.BaseContact, Singleton):
return None return None
return self._contacts[num] 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 # Work with active friend
# ----------------------------------------------------------------------------------------------------------------- # -----------------------------------------------------------------------------------------------------------------
@ -211,7 +214,7 @@ class Profile(basecontact.BaseContact, Singleton):
if value is not None: if value is not None:
if self._active_friend + 1 and self._active_friend != value: if self._active_friend + 1 and self._active_friend != value:
try: try:
self._contacts[self._active_friend].curr_text = self._screen.messageEdit.toPlainText() self.get_curr_friend().curr_text = self._screen.messageEdit.toPlainText()
except: except:
pass pass
friend = self._contacts[value] friend = self._contacts[value]
@ -261,7 +264,7 @@ class Profile(basecontact.BaseContact, Singleton):
else: else:
self._screen.call_finished() self._screen.call_finished()
else: else:
friend = self._contacts[self._active_friend] friend = self.get_curr_friend()
self._screen.account_name.setText(friend.name) self._screen.account_name.setText(friend.name)
self._screen.account_status.setText(friend.status_message) self._screen.account_status.setText(friend.status_message)
@ -287,18 +290,18 @@ class Profile(basecontact.BaseContact, Singleton):
def get_last_message(self): def get_last_message(self):
if self._active_friend + 1: 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: else:
return '' return ''
def get_active_number(self): 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): 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): 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): def new_name(self, number, name):
friend = self.get_friend_by_number(number) 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: if Settings.get_instance()['typing_notifications'] and self._active_friend + 1:
try: try:
friend = self._contacts[self._active_friend] friend = self.get_curr_friend()
if friend.status is not None: if friend.status is not None:
self._tox.self_set_typing(friend.number, typing) self._tox.self_set_typing(friend.number, typing)
except: except:
@ -443,7 +446,7 @@ class Profile(basecontact.BaseContact, Singleton):
t = time.time() t = time.time()
self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type) self.create_message_item(message, t, MESSAGE_OWNER['FRIEND'], message_type)
self._messages.scrollToBottom() self._messages.scrollToBottom()
self._contacts[self._active_friend].append_message( self.get_curr_friend().append_message(
TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type)) TextMessage(message, MESSAGE_OWNER['FRIEND'], t, message_type))
else: else:
friend = self.get_friend_by_number(friend_num) 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)) friend.append_message(TextMessage(text, MESSAGE_OWNER['NOT_SENT'], t, message_type))
def delete_message(self, time): def delete_message(self, time):
friend = self._contacts[self._active_friend] friend = self.get_curr_friend()
friend.delete_message(time) friend.delete_message(time)
self._history.delete_message(friend.tox_id, time) self._history.delete_message(friend.tox_id, time)
self.update() self.update()
@ -536,7 +539,7 @@ class Profile(basecontact.BaseContact, Singleton):
if not self._load_history: if not self._load_history:
return return
self._load_history = False self._load_history = False
friend = self._contacts[self._active_friend] friend = self.get_curr_friend()
friend.load_corr(False) friend.load_corr(False)
data = friend.get_corr() data = friend.get_corr()
if not data: if not data:
@ -624,7 +627,7 @@ class Profile(basecontact.BaseContact, Singleton):
pixmap = None pixmap = None
if self._show_avatars: if self._show_avatars:
if owner == MESSAGE_OWNER['FRIEND']: if owner == MESSAGE_OWNER['FRIEND']:
pixmap = self._contacts[self._active_friend].get_pixmap() pixmap = self.get_curr_friend().get_pixmap()
else: else:
pixmap = self.get_pixmap() pixmap = self.get_pixmap()
return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'], return self._factory.message_item(text, time, name, owner != MESSAGE_OWNER['NOT_SENT'],
@ -968,7 +971,7 @@ class Profile(basecontact.BaseContact, Singleton):
0, -1) 0, -1)
def cancel_not_started_transfer(self, time): 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() self.update()
def pause_transfer(self, friend_number, file_number, by_friend=False): def pause_transfer(self, friend_number, file_number, by_friend=False):
@ -1202,7 +1205,7 @@ class Profile(basecontact.BaseContact, Singleton):
else: else:
text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None, text = QtGui.QApplication.translate("incoming_call", "Outgoing audio call", None,
QtGui.QApplication.UnicodeUTF8) 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.create_message_item(text, time.time(), '', MESSAGE_TYPE['INFO_MESSAGE'])
self._messages.scrollToBottom() self._messages.scrollToBottom()
elif num in self._call: # finish or cancel call if you call with active friend elif num in self._call: # finish or cancel call if you call with active friend