fixed a bunch of stuff, added a notes programm, which lets you take notes, did some giant refactoring work and did some general designing
This commit is contained in:
373
src/IRCPanel.py
Normal file
373
src/IRCPanel.py
Normal file
@@ -0,0 +1,373 @@
|
||||
import wx
|
||||
import threading
|
||||
import logging
|
||||
from SearchDialog import SearchDialog
|
||||
import traceback
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class IRCPanel(wx.Panel):
|
||||
def __init__(self, parent, main_frame):
|
||||
super().__init__(parent)
|
||||
self.parent = parent
|
||||
self.main_frame = main_frame
|
||||
self.messages = []
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Use a better font for chat with white theme
|
||||
self.text_ctrl = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH2 | wx.TE_AUTO_URL)
|
||||
|
||||
# White theme colors
|
||||
self.text_ctrl.SetBackgroundColour(wx.Colour(255, 255, 255)) # White background
|
||||
self.text_ctrl.SetForegroundColour(wx.Colour(0, 0, 0)) # Black text
|
||||
|
||||
# Load appropriate font
|
||||
self.font = self.load_system_font()
|
||||
self.text_ctrl.SetFont(self.font)
|
||||
|
||||
sizer.Add(self.text_ctrl, 1, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
input_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.input_ctrl = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER)
|
||||
self.input_ctrl.SetHint("Type message here …")
|
||||
self.input_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_send)
|
||||
self.input_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_key_down)
|
||||
|
||||
send_btn = wx.Button(self, label="Send")
|
||||
send_btn.SetToolTip("Send message (Enter)")
|
||||
send_btn.Bind(wx.EVT_BUTTON, self.on_send)
|
||||
|
||||
input_sizer.Add(self.input_ctrl, 1, wx.EXPAND | wx.ALL, 2)
|
||||
input_sizer.Add(send_btn, 0, wx.ALL, 2)
|
||||
|
||||
sizer.Add(input_sizer, 0, wx.EXPAND | wx.ALL, 0)
|
||||
|
||||
self.SetSizer(sizer)
|
||||
self.target = None
|
||||
self.history = []
|
||||
self.history_pos = -1
|
||||
|
||||
# Search state
|
||||
self.search_text = ""
|
||||
self.search_positions = []
|
||||
self.current_search_index = -1
|
||||
|
||||
# Bind Ctrl+F for search
|
||||
self.text_ctrl.Bind(wx.EVT_KEY_DOWN, self.on_text_key_down)
|
||||
accel_tbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND)])
|
||||
self.SetAcceleratorTable(accel_tbl)
|
||||
self.Bind(wx.EVT_MENU, self.on_search, id=wx.ID_FIND)
|
||||
|
||||
def load_system_font(self):
|
||||
"""Load appropriate system font with high DPI support"""
|
||||
try:
|
||||
# Get system DPI scale factor
|
||||
dc = wx.ClientDC(self)
|
||||
dpi_scale = dc.GetPPI().GetWidth() / 96.0 # 96 is standard DPI
|
||||
|
||||
# Calculate base font size based on DPI
|
||||
base_size = 10
|
||||
if dpi_scale > 1.5:
|
||||
font_size = int(base_size * 1.5) # 150% scaling
|
||||
elif dpi_scale > 1.25:
|
||||
font_size = int(base_size * 1.25) # 125% scaling
|
||||
else:
|
||||
font_size = base_size
|
||||
|
||||
# Try system fonts in order of preference
|
||||
font_families = [
|
||||
(wx.FONTFAMILY_TELETYPE, "Consolas"),
|
||||
(wx.FONTFAMILY_TELETYPE, "Courier New"),
|
||||
(wx.FONTFAMILY_TELETYPE, "Monaco"),
|
||||
(wx.FONTFAMILY_TELETYPE, "DejaVu Sans Mono"),
|
||||
(wx.FONTFAMILY_TELETYPE, "Liberation Mono"),
|
||||
]
|
||||
|
||||
for family, face_name in font_families:
|
||||
font = wx.Font(font_size, family, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False, face_name)
|
||||
if font.IsOk():
|
||||
logger.info(f"Using font: {face_name} at {font_size}pt")
|
||||
return font
|
||||
|
||||
# Fallback to default monospace
|
||||
font = wx.Font(font_size, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||
logger.info("Using system monospace font as fallback")
|
||||
return font
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading system font: {e}")
|
||||
# Ultimate fallback
|
||||
return wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||
|
||||
def set_target(self, target):
|
||||
self.target = target
|
||||
|
||||
def add_message(self, message, username=None, username_color=None, message_color=None, bold=False, italic=False, underline=False):
|
||||
"""Thread-safe message addition with username coloring"""
|
||||
try:
|
||||
# Use CallAfter for thread safety
|
||||
if wx.IsMainThread():
|
||||
self._add_message_safe(message, username, username_color, message_color, bold, italic, underline)
|
||||
else:
|
||||
wx.CallAfter(self._add_message_safe, message, username, username_color, message_color, bold, italic, underline)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in add_message: {e}")
|
||||
|
||||
def _add_message_safe(self, message, username=None, username_color=None, message_color=None, bold=False, italic=False, underline=False):
|
||||
"""Actually add message - must be called from main thread"""
|
||||
try:
|
||||
self.messages.append(message)
|
||||
|
||||
# Save current position for formatting
|
||||
start_pos = self.text_ctrl.GetLastPosition()
|
||||
|
||||
if username and username_color:
|
||||
# Add username with its color
|
||||
attr = wx.TextAttr()
|
||||
attr.SetTextColour(username_color)
|
||||
if bold:
|
||||
attr.SetFontWeight(wx.FONTWEIGHT_BOLD)
|
||||
if italic:
|
||||
attr.SetFontStyle(wx.FONTSTYLE_ITALIC)
|
||||
if underline:
|
||||
attr.SetFontUnderlined(True)
|
||||
attr.SetFont(self.font)
|
||||
self.text_ctrl.SetDefaultStyle(attr)
|
||||
self.text_ctrl.AppendText(username)
|
||||
|
||||
# Add the rest of the message with message color
|
||||
attr = wx.TextAttr()
|
||||
if message_color:
|
||||
attr.SetTextColour(message_color)
|
||||
else:
|
||||
attr.SetTextColour(wx.Colour(0, 0, 0)) # Black text for white theme
|
||||
|
||||
attr.SetFont(self.font)
|
||||
self.text_ctrl.SetDefaultStyle(attr)
|
||||
|
||||
# Append the message (without username if we already added it)
|
||||
if username and username_color:
|
||||
# Find the message part after username
|
||||
message_text = message[message.find(username) + len(username):]
|
||||
self.text_ctrl.AppendText(message_text + "\n")
|
||||
else:
|
||||
self.text_ctrl.AppendText(message + "\n")
|
||||
|
||||
# Auto-scroll to bottom
|
||||
self.text_ctrl.ShowPosition(self.text_ctrl.GetLastPosition())
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding message safely: {e}")
|
||||
|
||||
def add_formatted_message(self, timestamp, username, content, username_color=None, is_action=False):
|
||||
"""Add a formatted message with colored username"""
|
||||
try:
|
||||
if is_action:
|
||||
message = f"{timestamp}* {username} {content}"
|
||||
self.add_message(message, f"* {username}", username_color, wx.Colour(128, 0, 128), italic=True) # Dark purple for actions
|
||||
else:
|
||||
message = f"{timestamp}<{username}> {content}"
|
||||
self.add_message(message, f"<{username}>", username_color, wx.Colour(0, 0, 0)) # Black text
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding formatted message: {e}")
|
||||
|
||||
def add_system_message(self, message, color=None, bold=False):
|
||||
"""Add system message without username coloring"""
|
||||
try:
|
||||
if color is None:
|
||||
color = wx.Colour(0, 0, 128) # Dark blue for system messages
|
||||
self.add_message(message, None, None, color, bold, False, False)
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding system message: {e}")
|
||||
|
||||
def on_text_key_down(self, event):
|
||||
"""Handle key events in the text control"""
|
||||
keycode = event.GetKeyCode()
|
||||
if event.ControlDown() and keycode == ord('F'):
|
||||
self.on_search(event)
|
||||
else:
|
||||
event.Skip()
|
||||
|
||||
def on_search(self, event):
|
||||
"""Open search dialog"""
|
||||
try:
|
||||
dlg = SearchDialog(self)
|
||||
dlg.ShowModal()
|
||||
dlg.Destroy()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in search: {e}")
|
||||
|
||||
def perform_search(self, search_text, case_sensitive=False, whole_word=False):
|
||||
"""Perform text search in the chat history"""
|
||||
try:
|
||||
self.search_text = search_text
|
||||
self.search_positions = []
|
||||
self.current_search_index = -1
|
||||
|
||||
# Get all text
|
||||
full_text = self.text_ctrl.GetValue()
|
||||
if not full_text or not search_text:
|
||||
return
|
||||
|
||||
# Prepare search parameters
|
||||
search_flags = 0
|
||||
if not case_sensitive:
|
||||
# For manual search, we'll handle case sensitivity ourselves
|
||||
search_text_lower = search_text.lower()
|
||||
full_text_lower = full_text.lower()
|
||||
|
||||
# Manual search implementation since wx.TextCtrl doesn't have FindText
|
||||
pos = 0
|
||||
while pos < len(full_text):
|
||||
if case_sensitive:
|
||||
# Case sensitive search
|
||||
found_pos = full_text.find(search_text, pos)
|
||||
else:
|
||||
# Case insensitive search
|
||||
found_pos = full_text_lower.find(search_text_lower, pos)
|
||||
|
||||
if found_pos == -1:
|
||||
break
|
||||
|
||||
# For whole word search, verify boundaries
|
||||
if whole_word:
|
||||
# Check if it's a whole word
|
||||
is_word_start = (found_pos == 0 or not full_text[found_pos-1].isalnum())
|
||||
is_word_end = (found_pos + len(search_text) >= len(full_text) or
|
||||
not full_text[found_pos + len(search_text)].isalnum())
|
||||
|
||||
if is_word_start and is_word_end:
|
||||
self.search_positions.append(found_pos)
|
||||
pos = found_pos + 1 # Move forward to avoid infinite loop
|
||||
else:
|
||||
self.search_positions.append(found_pos)
|
||||
pos = found_pos + len(search_text)
|
||||
|
||||
if self.search_positions:
|
||||
self.current_search_index = 0
|
||||
self.highlight_search_result()
|
||||
self.main_frame.SetStatusText(f"Found {len(self.search_positions)} occurrences of '{search_text}'")
|
||||
else:
|
||||
self.main_frame.SetStatusText(f"Text '{search_text}' not found")
|
||||
wx.Bell()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error performing search: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
def highlight_search_result(self):
|
||||
"""Highlight the current search result"""
|
||||
try:
|
||||
if not self.search_positions or self.current_search_index < 0:
|
||||
return
|
||||
|
||||
pos = self.search_positions[self.current_search_index]
|
||||
|
||||
# Select the found text
|
||||
self.text_ctrl.SetSelection(pos, pos + len(self.search_text))
|
||||
self.text_ctrl.ShowPosition(pos)
|
||||
|
||||
# Update status
|
||||
self.main_frame.SetStatusText(
|
||||
f"Found {self.current_search_index + 1} of {len(self.search_positions)}: '{self.search_text}'"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error highlighting search result: {e}")
|
||||
|
||||
def find_next(self):
|
||||
"""Find next occurrence"""
|
||||
if self.search_positions:
|
||||
self.current_search_index = (self.current_search_index + 1) % len(self.search_positions)
|
||||
self.highlight_search_result()
|
||||
|
||||
def find_previous(self):
|
||||
"""Find previous occurrence"""
|
||||
if self.search_positions:
|
||||
self.current_search_index = (self.current_search_index - 1) % len(self.search_positions)
|
||||
self.highlight_search_result()
|
||||
|
||||
def on_key_down(self, event):
|
||||
try:
|
||||
keycode = event.GetKeyCode()
|
||||
if keycode == wx.WXK_UP:
|
||||
if self.history and self.history_pos < len(self.history) - 1:
|
||||
self.history_pos += 1
|
||||
self.input_ctrl.SetValue(self.history[-(self.history_pos + 1)])
|
||||
elif keycode == wx.WXK_DOWN:
|
||||
if self.history_pos > 0:
|
||||
self.history_pos -= 1
|
||||
self.input_ctrl.SetValue(self.history[-(self.history_pos + 1)])
|
||||
elif self.history_pos == 0:
|
||||
self.history_pos = -1
|
||||
self.input_ctrl.Clear()
|
||||
elif keycode == wx.WXK_TAB:
|
||||
# Tab completion for nicknames
|
||||
self.handle_tab_completion()
|
||||
return # Don't skip to prevent default tab behavior
|
||||
elif keycode == wx.WXK_F3:
|
||||
# F3 for find next
|
||||
if self.search_positions:
|
||||
self.find_next()
|
||||
return
|
||||
elif event.ShiftDown() and keycode == wx.WXK_F3:
|
||||
# Shift+F3 for find previous
|
||||
if self.search_positions:
|
||||
self.find_previous()
|
||||
return
|
||||
else:
|
||||
event.Skip()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in key handler: {e}")
|
||||
event.Skip()
|
||||
|
||||
def handle_tab_completion(self):
|
||||
"""Handle tab completion for nicknames"""
|
||||
try:
|
||||
current_text = self.input_ctrl.GetValue()
|
||||
if not current_text or not self.target or not self.target.startswith('#'):
|
||||
return
|
||||
|
||||
users = self.main_frame.channel_users.get(self.target, [])
|
||||
if not users:
|
||||
return
|
||||
|
||||
# Find word at cursor position
|
||||
pos = self.input_ctrl.GetInsertionPoint()
|
||||
text_before = current_text[:pos]
|
||||
words = text_before.split()
|
||||
|
||||
if not words:
|
||||
return
|
||||
|
||||
current_word = words[-1]
|
||||
|
||||
# Find matching nicks
|
||||
matches = [user for user in users if user.lower().startswith(current_word.lower())]
|
||||
|
||||
if matches:
|
||||
if len(matches) == 1:
|
||||
# Single match - complete it
|
||||
new_word = matches[0]
|
||||
if ':' in text_before or text_before.strip().endswith(current_word):
|
||||
# Replace the current word
|
||||
new_text = text_before[:-len(current_word)] + new_word + current_text[pos:]
|
||||
self.input_ctrl.SetValue(new_text)
|
||||
self.input_ctrl.SetInsertionPoint(pos - len(current_word) + len(new_word))
|
||||
else:
|
||||
# Multiple matches - show in status
|
||||
self.main_frame.SetStatusText(f"Tab completion: {', '.join(matches[:5])}{'...' if len(matches) > 5 else ''}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in tab completion: {e}")
|
||||
|
||||
def on_send(self, event):
|
||||
try:
|
||||
message = self.input_ctrl.GetValue().strip()
|
||||
if message and self.target:
|
||||
self.history.append(message)
|
||||
self.history_pos = -1
|
||||
self.main_frame.send_message(self.target, message)
|
||||
self.input_ctrl.Clear()
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message: {e}")
|
||||
Reference in New Issue
Block a user