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:
18
build.ps1
Normal file
18
build.ps1
Normal file
@@ -0,0 +1,18 @@
|
||||
Write-Host "Building application with PyInstaller..." -ForegroundColor Cyan
|
||||
|
||||
# Activate venv
|
||||
& "$PSScriptRoot\venv\Scripts\Activate.ps1"
|
||||
|
||||
pyinstaller `
|
||||
--onefile `
|
||||
--noconfirm `
|
||||
--windowed `
|
||||
--hidden-import wx._xml `
|
||||
--add-data "FiraCode-Regular.ttf;." `
|
||||
--add-data "FiraCode-SemiBold.ttf;." `
|
||||
--add-data "venv\Lib\site-packages\irc\codes.txt;irc" `
|
||||
--add-data "icon.ico;." `
|
||||
--icon "icon.ico" `
|
||||
"src/main.py"
|
||||
|
||||
Write-Host "Build complete!" -ForegroundColor Green
|
||||
20
build.sh
20
build.sh
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
pyinstaller \
|
||||
--onefile \
|
||||
--noconfirm \
|
||||
--add-data "FiraCode-Regular.ttf:." \
|
||||
--add-data "FiraCode-SemiBold.ttf:." \
|
||||
--add-data "$(python -c 'import irc, os; print(os.path.dirname(irc.__file__))'):irc" \
|
||||
--hidden-import=irc.client \
|
||||
--hidden-import=irc.connection \
|
||||
--hidden-import=irc.events \
|
||||
--hidden-import=irc.strings \
|
||||
--hidden-import=wx \
|
||||
--hidden-import=wx.lib.mixins.listctrl \
|
||||
--hidden-import=wx.lib.mixins.treemixin \
|
||||
--hidden-import=wx.lib.mixins.inspection \
|
||||
--hidden-import=psutil \
|
||||
--hidden-import=queue \
|
||||
--hidden-import=logging.handlers \
|
||||
main.py
|
||||
BIN
icon.ico
BIN
icon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 168 KiB |
57
src/AboutDialog.py
Normal file
57
src/AboutDialog.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import wx
|
||||
import sys
|
||||
import os
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""Get absolute path to resource, works for dev and for PyInstaller"""
|
||||
try:
|
||||
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
||||
base_path = sys._MEIPASS
|
||||
except Exception:
|
||||
base_path = os.path.abspath(".")
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
class AboutDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent, title="About wxIRC Client", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
icon_path = get_resource_path("icon.ico")
|
||||
icon_bitmap = wx.Bitmap(icon_path, wx.BITMAP_TYPE_ICO)
|
||||
icon_ctrl = wx.StaticBitmap(self, bitmap=icon_bitmap)
|
||||
|
||||
# Add the icon to the sizer
|
||||
sizer.Add(icon_ctrl, 0, wx.ALL | wx.ALIGN_CENTER, 10)
|
||||
|
||||
# Application info
|
||||
info_text = wx.StaticText(self, label="wxIRC Client")
|
||||
info_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
info_text.SetFont(info_font)
|
||||
|
||||
version_text = wx.StaticText(self, label="V 1.1.1.0")
|
||||
version_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||
version_text.SetFont(version_font)
|
||||
|
||||
contrubutors_text = wx.StaticText(self, label="MiT License")
|
||||
contrubutors_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||
contrubutors_text.SetFont(contrubutors_font)
|
||||
|
||||
# Add info to sizer
|
||||
sizer.Add(info_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
|
||||
sizer.Add(version_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
|
||||
sizer.Add(contrubutors_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
|
||||
|
||||
# OK button
|
||||
ok_btn = wx.Button(self, wx.ID_OK, "OK")
|
||||
ok_btn.SetDefault()
|
||||
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btn_sizer.Add(ok_btn, 0, wx.ALIGN_CENTER | wx.ALL, 10)
|
||||
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
|
||||
|
||||
self.SetSizer(sizer)
|
||||
self.Fit()
|
||||
self.Centre()
|
||||
|
||||
BIN
src/FiraCode-Regular.ttf
Normal file
BIN
src/FiraCode-Regular.ttf
Normal file
Binary file not shown.
BIN
src/FiraCode-SemiBold.ttf
Normal file
BIN
src/FiraCode-SemiBold.ttf
Normal file
Binary file not shown.
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}")
|
||||
811
src/NotesDialog.py
Normal file
811
src/NotesDialog.py
Normal file
@@ -0,0 +1,811 @@
|
||||
import wx
|
||||
import wx.richtext as rt
|
||||
import json
|
||||
from collections import defaultdict
|
||||
import tempfile
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class NotesDialog(wx.Frame):
|
||||
def __init__(self, parent, notes_data=None, pos=None):
|
||||
# If no position specified, offset from parent
|
||||
if pos is None and parent:
|
||||
parent_pos = parent.GetPosition()
|
||||
pos = (parent_pos.x + 50, parent_pos.y + 50)
|
||||
|
||||
super().__init__(parent, title="IRC Notes", size=(900, 650), pos=pos,
|
||||
style=wx.DEFAULT_FRAME_STYLE)
|
||||
|
||||
self.parent = parent
|
||||
self.notes_data = notes_data or defaultdict(dict)
|
||||
self.current_note_key = None
|
||||
self.updating_title = False
|
||||
self.is_closing = False
|
||||
self.content_changed = False
|
||||
self.last_save_time = time.time()
|
||||
self.auto_save_interval = 2 # seconds - reduced for immediate saving
|
||||
|
||||
self.SetBackgroundColour(wx.Colour(245, 245, 245))
|
||||
|
||||
# Set icon if parent has one
|
||||
if parent:
|
||||
self.SetIcon(parent.GetIcon())
|
||||
|
||||
self.create_controls()
|
||||
self.load_notes_list()
|
||||
|
||||
# Bind close event to save before closing
|
||||
self.Bind(wx.EVT_CLOSE, self.on_close)
|
||||
|
||||
# Auto-save timer (save every 2 seconds)
|
||||
self.save_timer = wx.Timer(self)
|
||||
self.Bind(wx.EVT_TIMER, self.on_auto_save, self.save_timer)
|
||||
self.save_timer.Start(self.auto_save_interval * 1000) # Convert to milliseconds
|
||||
|
||||
# Status update timer (update status more frequently)
|
||||
self.status_timer = wx.Timer(self)
|
||||
self.Bind(wx.EVT_TIMER, self.on_status_update, self.status_timer)
|
||||
self.status_timer.Start(3000) # 3 seconds
|
||||
|
||||
# Initialize status
|
||||
self.update_status("Ready")
|
||||
|
||||
def create_controls(self):
|
||||
# Create menu bar
|
||||
self.create_menu_bar()
|
||||
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Main content area with splitter
|
||||
splitter = wx.SplitterWindow(self, style=wx.SP_3D | wx.SP_LIVE_UPDATE)
|
||||
|
||||
# Left panel - notes list
|
||||
left_panel = wx.Panel(splitter)
|
||||
left_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
notes_label = wx.StaticText(left_panel, label="Your Notes:")
|
||||
font = notes_label.GetFont()
|
||||
font.PointSize += 1
|
||||
font = font.Bold()
|
||||
notes_label.SetFont(font)
|
||||
left_sizer.Add(notes_label, 0, wx.ALL, 8)
|
||||
|
||||
self.notes_list = wx.ListBox(left_panel, style=wx.LB_SINGLE)
|
||||
self.notes_list.Bind(wx.EVT_LISTBOX, self.on_note_select)
|
||||
self.notes_list.Bind(wx.EVT_LISTBOX_DCLICK, self.on_note_double_click)
|
||||
left_sizer.Add(self.notes_list, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||
|
||||
# Note management buttons
|
||||
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.new_btn = wx.Button(left_panel, label="New Note")
|
||||
self.new_btn.Bind(wx.EVT_BUTTON, self.on_new_note)
|
||||
btn_sizer.Add(self.new_btn, 1, wx.ALL, 3)
|
||||
|
||||
self.delete_btn = wx.Button(left_panel, label="Delete")
|
||||
self.delete_btn.Bind(wx.EVT_BUTTON, self.on_delete_note)
|
||||
btn_sizer.Add(self.delete_btn, 1, wx.ALL, 3)
|
||||
|
||||
left_sizer.Add(btn_sizer, 0, wx.EXPAND | wx.ALL, 8)
|
||||
left_panel.SetSizer(left_sizer)
|
||||
|
||||
# Right panel - editor
|
||||
right_panel = wx.Panel(splitter)
|
||||
right_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
title_label = wx.StaticText(right_panel, label="Note Title:")
|
||||
title_label.SetFont(font)
|
||||
right_sizer.Add(title_label, 0, wx.ALL, 8)
|
||||
|
||||
self.title_ctrl = wx.TextCtrl(right_panel, style=wx.TE_PROCESS_ENTER)
|
||||
self.title_ctrl.Bind(wx.EVT_TEXT, self.on_title_change)
|
||||
self.title_ctrl.Bind(wx.EVT_KILL_FOCUS, self.on_title_lose_focus)
|
||||
title_font = self.title_ctrl.GetFont()
|
||||
title_font.PointSize += 2
|
||||
self.title_ctrl.SetFont(title_font)
|
||||
right_sizer.Add(self.title_ctrl, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||
|
||||
content_label = wx.StaticText(right_panel, label="Content:")
|
||||
content_label.SetFont(font)
|
||||
right_sizer.Add(content_label, 0, wx.LEFT | wx.RIGHT, 8)
|
||||
|
||||
# Formatting toolbar
|
||||
self.setup_editor_toolbar(right_panel)
|
||||
right_sizer.Add(self.toolbar, 0, wx.EXPAND | wx.ALL, 8)
|
||||
|
||||
# Rich text editor - FIXED: Make sure editor attribute is created
|
||||
self.editor = rt.RichTextCtrl(right_panel,
|
||||
style=wx.VSCROLL | wx.HSCROLL | wx.BORDER_SIMPLE)
|
||||
self.editor.Bind(wx.EVT_TEXT, self.on_content_change)
|
||||
self.editor.Bind(wx.EVT_KILL_FOCUS, self.on_editor_lose_focus)
|
||||
|
||||
# Set default font
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetFontSize(11)
|
||||
attr.SetFontFaceName("Segoe UI")
|
||||
self.editor.SetBasicStyle(attr)
|
||||
|
||||
right_sizer.Add(self.editor, 1, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 8)
|
||||
right_panel.SetSizer(right_sizer)
|
||||
|
||||
# Split the window
|
||||
splitter.SplitVertically(left_panel, right_panel, 250)
|
||||
splitter.SetMinimumPaneSize(200)
|
||||
|
||||
main_sizer.Add(splitter, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Status bar
|
||||
self.status_bar = self.CreateStatusBar(2)
|
||||
self.status_bar.SetStatusWidths([-1, 150])
|
||||
self.update_status("Ready")
|
||||
|
||||
self.SetSizer(main_sizer)
|
||||
self.enable_editor(False)
|
||||
|
||||
def create_menu_bar(self):
|
||||
menubar = wx.MenuBar()
|
||||
|
||||
# File menu
|
||||
file_menu = wx.Menu()
|
||||
export_item = file_menu.Append(wx.ID_ANY, "&Export Notes...\tCtrl+E", "Export all notes to file")
|
||||
import_item = file_menu.Append(wx.ID_ANY, "&Import Notes...\tCtrl+I", "Import notes from file")
|
||||
file_menu.AppendSeparator()
|
||||
export_text_item = file_menu.Append(wx.ID_ANY, "Export Current Note as &Text...\tCtrl+T", "Export current note as plain text")
|
||||
file_menu.AppendSeparator()
|
||||
exit_item = file_menu.Append(wx.ID_EXIT, "&Close", "Close notes window")
|
||||
|
||||
menubar.Append(file_menu, "&File")
|
||||
|
||||
# Notes menu
|
||||
notes_menu = wx.Menu()
|
||||
new_note_item = notes_menu.Append(wx.ID_NEW, "&New Note\tCtrl+N", "Create a new note")
|
||||
delete_note_item = notes_menu.Append(wx.ID_DELETE, "&Delete Note\tDel", "Delete current note")
|
||||
notes_menu.AppendSeparator()
|
||||
rename_note_item = notes_menu.Append(wx.ID_ANY, "&Rename Note\tF2", "Rename current note")
|
||||
|
||||
menubar.Append(notes_menu, "&Notes")
|
||||
|
||||
# Edit menu
|
||||
edit_menu = wx.Menu()
|
||||
undo_item = edit_menu.Append(wx.ID_UNDO, "&Undo\tCtrl+Z", "Undo last action")
|
||||
redo_item = edit_menu.Append(wx.ID_REDO, "&Redo\tCtrl+Y", "Redo last action")
|
||||
edit_menu.AppendSeparator()
|
||||
cut_item = edit_menu.Append(wx.ID_CUT, "Cu&t\tCtrl+X", "Cut selection")
|
||||
copy_item = edit_menu.Append(wx.ID_COPY, "&Copy\tCtrl+C", "Copy selection")
|
||||
paste_item = edit_menu.Append(wx.ID_PASTE, "&Paste\tCtrl+V", "Paste from clipboard")
|
||||
edit_menu.AppendSeparator()
|
||||
select_all_item = edit_menu.Append(wx.ID_SELECTALL, "Select &All\tCtrl+A", "Select all text")
|
||||
|
||||
menubar.Append(edit_menu, "&Edit")
|
||||
|
||||
# Format menu
|
||||
format_menu = wx.Menu()
|
||||
bold_item = format_menu.Append(wx.ID_ANY, "&Bold\tCtrl+B", "Bold text")
|
||||
italic_item = format_menu.Append(wx.ID_ANY, "&Italic\tCtrl+I", "Italic text")
|
||||
underline_item = format_menu.Append(wx.ID_ANY, "&Underline\tCtrl+U", "Underline text")
|
||||
format_menu.AppendSeparator()
|
||||
font_color_item = format_menu.Append(wx.ID_ANY, "&Text Color...", "Change text color")
|
||||
bg_color_item = format_menu.Append(wx.ID_ANY, "&Highlight Color...", "Change highlight color")
|
||||
format_menu.AppendSeparator()
|
||||
clear_format_item = format_menu.Append(wx.ID_ANY, "&Clear Formatting\tCtrl+Space", "Clear text formatting")
|
||||
|
||||
menubar.Append(format_menu, "F&ormat")
|
||||
|
||||
self.SetMenuBar(menubar)
|
||||
|
||||
# Bind menu events
|
||||
self.Bind(wx.EVT_MENU, self.on_save_to_file, export_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_load_from_file, import_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_export_text, export_text_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_close, exit_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_new_note, new_note_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_delete_note, delete_note_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_rename_note, rename_note_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_bold, bold_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_italic, italic_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_underline, underline_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_text_color, font_color_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_background_color, bg_color_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_clear_formatting, clear_format_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_undo, undo_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_redo, redo_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_cut, cut_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_copy, copy_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_paste, paste_item)
|
||||
self.Bind(wx.EVT_MENU, self.on_select_all, select_all_item)
|
||||
|
||||
def setup_editor_toolbar(self, parent):
|
||||
self.toolbar = wx.Panel(parent)
|
||||
toolbar_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
|
||||
# Text formatting buttons
|
||||
self.bold_btn = wx.Button(self.toolbar, label="B", size=(35, 30))
|
||||
self.bold_btn.SetFont(self.bold_btn.GetFont().Bold())
|
||||
self.bold_btn.Bind(wx.EVT_BUTTON, self.on_bold)
|
||||
self.bold_btn.SetToolTip("Bold (Ctrl+B)")
|
||||
toolbar_sizer.Add(self.bold_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.italic_btn = wx.Button(self.toolbar, label="I", size=(35, 30))
|
||||
font = self.italic_btn.GetFont()
|
||||
font.MakeItalic()
|
||||
self.italic_btn.SetFont(font)
|
||||
self.italic_btn.Bind(wx.EVT_BUTTON, self.on_italic)
|
||||
self.italic_btn.SetToolTip("Italic (Ctrl+I)")
|
||||
toolbar_sizer.Add(self.italic_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.underline_btn = wx.Button(self.toolbar, label="U", size=(35, 30))
|
||||
self.underline_btn.Bind(wx.EVT_BUTTON, self.on_underline)
|
||||
self.underline_btn.SetToolTip("Underline (Ctrl+U)")
|
||||
toolbar_sizer.Add(self.underline_btn, 0, wx.ALL, 2)
|
||||
|
||||
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
|
||||
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
|
||||
|
||||
# Font size
|
||||
toolbar_sizer.Add(wx.StaticText(self.toolbar, label="Size:"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
|
||||
self.font_size_choice = wx.Choice(self.toolbar, choices=['8', '9', '10', '11', '12', '14', '16', '18', '20', '24'])
|
||||
self.font_size_choice.SetSelection(3) # Default to 11
|
||||
self.font_size_choice.Bind(wx.EVT_CHOICE, self.on_font_size)
|
||||
self.font_size_choice.SetToolTip("Font Size")
|
||||
toolbar_sizer.Add(self.font_size_choice, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2)
|
||||
|
||||
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
|
||||
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
|
||||
|
||||
# Alignment buttons
|
||||
self.align_left_btn = wx.Button(self.toolbar, label="Left", size=(45, 30))
|
||||
self.align_left_btn.Bind(wx.EVT_BUTTON, self.on_align_left)
|
||||
self.align_left_btn.SetToolTip("Align Left")
|
||||
toolbar_sizer.Add(self.align_left_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.align_center_btn = wx.Button(self.toolbar, label="Center", size=(55, 30))
|
||||
self.align_center_btn.Bind(wx.EVT_BUTTON, self.on_align_center)
|
||||
self.align_center_btn.SetToolTip("Align Center")
|
||||
toolbar_sizer.Add(self.align_center_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.align_right_btn = wx.Button(self.toolbar, label="Right", size=(45, 30))
|
||||
self.align_right_btn.Bind(wx.EVT_BUTTON, self.on_align_right)
|
||||
self.align_right_btn.SetToolTip("Align Right")
|
||||
toolbar_sizer.Add(self.align_right_btn, 0, wx.ALL, 2)
|
||||
|
||||
toolbar_sizer.Add(wx.StaticLine(self.toolbar, style=wx.LI_VERTICAL), 0,
|
||||
wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
|
||||
|
||||
# Color buttons
|
||||
self.text_color_btn = wx.Button(self.toolbar, label="Text Color", size=(80, 30))
|
||||
self.text_color_btn.Bind(wx.EVT_BUTTON, self.on_text_color)
|
||||
self.text_color_btn.SetToolTip("Text Color")
|
||||
toolbar_sizer.Add(self.text_color_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.bg_color_btn = wx.Button(self.toolbar, label="Highlight", size=(80, 30))
|
||||
self.bg_color_btn.Bind(wx.EVT_BUTTON, self.on_background_color)
|
||||
self.bg_color_btn.SetToolTip("Highlight Color")
|
||||
toolbar_sizer.Add(self.bg_color_btn, 0, wx.ALL, 2)
|
||||
|
||||
toolbar_sizer.AddStretchSpacer()
|
||||
|
||||
# Clear formatting
|
||||
self.clear_fmt_btn = wx.Button(self.toolbar, label="Clear Format", size=(100, 30))
|
||||
self.clear_fmt_btn.Bind(wx.EVT_BUTTON, self.on_clear_formatting)
|
||||
self.clear_fmt_btn.SetToolTip("Remove all formatting (Ctrl+Space)")
|
||||
toolbar_sizer.Add(self.clear_fmt_btn, 0, wx.ALL, 2)
|
||||
|
||||
self.toolbar.SetSizer(toolbar_sizer)
|
||||
self.toolbar.SetBackgroundColour(wx.Colour(230, 230, 230))
|
||||
|
||||
def enable_editor(self, enable):
|
||||
self.title_ctrl.Enable(enable)
|
||||
self.editor.Enable(enable)
|
||||
self.delete_btn.Enable(enable)
|
||||
|
||||
# Enable/disable toolbar buttons
|
||||
for child in self.toolbar.GetChildren():
|
||||
if isinstance(child, wx.Button) or isinstance(child, wx.Choice):
|
||||
child.Enable(enable)
|
||||
|
||||
def update_status(self, message: str):
|
||||
"""Update status text with timestamp"""
|
||||
timestamp = wx.DateTime.Now().FormatTime()
|
||||
self.status_bar.SetStatusText(f"{timestamp} - {message}", 0)
|
||||
|
||||
# Update save status in second field
|
||||
if self.current_note_key and self.content_changed:
|
||||
time_since_change = int(time.time() - self.notes_data[self.current_note_key]['last_modified'])
|
||||
self.status_bar.SetStatusText(f"Unsaved changes ({time_since_change}s)", 1)
|
||||
elif self.current_note_key:
|
||||
time_since_save = int(time.time() - self.last_save_time)
|
||||
self.status_bar.SetStatusText(f"Saved {time_since_save}s ago", 1)
|
||||
else:
|
||||
self.status_bar.SetStatusText("", 1)
|
||||
|
||||
def load_notes_list(self):
|
||||
self.notes_list.Clear()
|
||||
sorted_keys = sorted(self.notes_data.keys(),
|
||||
key=lambda k: self.notes_data[k].get('last_modified', 0),
|
||||
reverse=True)
|
||||
for key in sorted_keys:
|
||||
display_title = self.notes_data[key].get('title', key)
|
||||
self.notes_list.Append(display_title, key)
|
||||
|
||||
def save_current_note_formatting(self) -> bool:
|
||||
"""Save the current note's formatting to XML"""
|
||||
if not self.current_note_key or self.is_closing:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Save plain text
|
||||
self.notes_data[self.current_note_key]['content'] = self.editor.GetValue()
|
||||
|
||||
# Save XML with formatting using a temporary file
|
||||
with tempfile.NamedTemporaryFile(mode='w+', suffix='.xml', delete=False, encoding='utf-8') as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Save to temp file
|
||||
handler = rt.RichTextXMLHandler()
|
||||
buffer = self.editor.GetBuffer()
|
||||
|
||||
if buffer and handler.SaveFile(buffer, tmp_path):
|
||||
# Read back the XML content
|
||||
with open(tmp_path, 'r', encoding='utf-8') as f:
|
||||
xml_content = f.read()
|
||||
self.notes_data[self.current_note_key]['xml_content'] = xml_content
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.content_changed = False
|
||||
self.last_save_time = time.time()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error saving note formatting: {e}")
|
||||
return False
|
||||
|
||||
def auto_save_current_note(self):
|
||||
"""Auto-save current note if changes were made"""
|
||||
if self.current_note_key and self.content_changed:
|
||||
if self.save_current_note_formatting():
|
||||
return True
|
||||
return False
|
||||
|
||||
def safe_load_xml_content(self, xml_content: str) -> bool:
|
||||
"""Safely load XML content with error handling"""
|
||||
try:
|
||||
# Create a temporary file for XML loading
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False, encoding='utf-8') as tmp:
|
||||
tmp.write(xml_content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
# Clear current buffer first
|
||||
self.editor.Clear()
|
||||
|
||||
# Load from temp file
|
||||
handler = rt.RichTextXMLHandler()
|
||||
success = handler.LoadFile(self.editor.GetBuffer(), tmp_path)
|
||||
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if success:
|
||||
self.editor.Refresh()
|
||||
return True
|
||||
else:
|
||||
# If XML load fails, fall back to plain text
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading XML content: {e}")
|
||||
return False
|
||||
|
||||
def on_note_select(self, event):
|
||||
# Auto-save previous note before switching
|
||||
if self.current_note_key:
|
||||
self.auto_save_current_note()
|
||||
|
||||
selection = self.notes_list.GetSelection()
|
||||
if selection != wx.NOT_FOUND:
|
||||
key = self.notes_list.GetClientData(selection)
|
||||
self.current_note_key = key
|
||||
note_data = self.notes_data[key]
|
||||
|
||||
self.updating_title = True
|
||||
self.title_ctrl.SetValue(note_data.get('title', ''))
|
||||
self.updating_title = False
|
||||
|
||||
# Load content - check if it's XML format or plain text
|
||||
content = note_data.get('content', '')
|
||||
xml_content = note_data.get('xml_content', '')
|
||||
|
||||
# Clear any existing content first
|
||||
self.editor.Clear()
|
||||
|
||||
if xml_content:
|
||||
# Try to load rich text from XML
|
||||
if not self.safe_load_xml_content(xml_content):
|
||||
# Fallback to plain text if XML load fails
|
||||
self.editor.SetValue(content)
|
||||
elif content:
|
||||
# Fallback to plain text
|
||||
self.editor.SetValue(content)
|
||||
else:
|
||||
self.editor.Clear()
|
||||
|
||||
self.enable_editor(True)
|
||||
self.editor.SetFocus()
|
||||
self.content_changed = False
|
||||
self.update_status(f"Loaded note: {note_data.get('title', 'Untitled')}")
|
||||
|
||||
def on_note_double_click(self, event):
|
||||
"""Rename note on double click"""
|
||||
self.on_rename_note(event)
|
||||
|
||||
def on_new_note(self, event):
|
||||
note_id = f"note_{int(time.time() * 1000)}" # More precise timestamp
|
||||
title = f"New Note {len(self.notes_data) + 1}"
|
||||
self.notes_data[note_id] = {
|
||||
'title': title,
|
||||
'content': '',
|
||||
'xml_content': '',
|
||||
'created': time.time(),
|
||||
'last_modified': time.time()
|
||||
}
|
||||
self.load_notes_list()
|
||||
|
||||
# Select and edit the new note
|
||||
for i in range(self.notes_list.GetCount()):
|
||||
if self.notes_list.GetClientData(i) == note_id:
|
||||
self.notes_list.SetSelection(i)
|
||||
self.on_note_select(event)
|
||||
self.title_ctrl.SetFocus()
|
||||
self.title_ctrl.SelectAll()
|
||||
break
|
||||
|
||||
def on_delete_note(self, event):
|
||||
if self.current_note_key:
|
||||
note_title = self.notes_data[self.current_note_key].get('title', 'this note')
|
||||
result = wx.MessageBox(
|
||||
f"Are you sure you want to delete '{note_title}'?",
|
||||
"Confirm Delete",
|
||||
wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION
|
||||
)
|
||||
if result == wx.YES:
|
||||
del self.notes_data[self.current_note_key]
|
||||
self.current_note_key = None
|
||||
self.load_notes_list()
|
||||
self.title_ctrl.SetValue('')
|
||||
self.editor.Clear()
|
||||
self.enable_editor(False)
|
||||
self.update_status("Note deleted")
|
||||
|
||||
def on_rename_note(self, event):
|
||||
"""Rename current note by focusing on title field"""
|
||||
if self.current_note_key:
|
||||
self.title_ctrl.SetFocus()
|
||||
self.title_ctrl.SelectAll()
|
||||
|
||||
def on_title_change(self, event):
|
||||
if self.current_note_key and not self.updating_title:
|
||||
new_title = self.title_ctrl.GetValue().strip()
|
||||
if new_title:
|
||||
self.notes_data[self.current_note_key]['title'] = new_title
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
# Update the display in the list
|
||||
for i in range(self.notes_list.GetCount()):
|
||||
if self.notes_list.GetClientData(i) == self.current_note_key:
|
||||
self.notes_list.SetString(i, new_title)
|
||||
break
|
||||
|
||||
self.content_changed = True
|
||||
# Auto-save immediately on title change
|
||||
self.auto_save_current_note()
|
||||
self.update_status("Title updated")
|
||||
|
||||
def on_title_lose_focus(self, event):
|
||||
"""Auto-save when title loses focus"""
|
||||
if self.content_changed:
|
||||
self.auto_save_current_note()
|
||||
event.Skip()
|
||||
|
||||
def on_content_change(self, event):
|
||||
"""Mark content as changed for auto-save"""
|
||||
if self.current_note_key:
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
# Trigger immediate auto-save on content change
|
||||
wx.CallLater(1000, self.auto_save_current_note) # Save after 1 second delay
|
||||
event.Skip()
|
||||
|
||||
def on_editor_lose_focus(self, event):
|
||||
"""Auto-save when editor loses focus"""
|
||||
if self.content_changed:
|
||||
self.auto_save_current_note()
|
||||
event.Skip()
|
||||
|
||||
def on_auto_save(self, event):
|
||||
"""Auto-save current note on timer"""
|
||||
if self.auto_save_current_note():
|
||||
self.update_status(f"Auto-saved at {wx.DateTime.Now().FormatTime()}")
|
||||
|
||||
def on_status_update(self, event):
|
||||
"""Update status bar"""
|
||||
self.update_status("Ready")
|
||||
|
||||
# Text formatting methods
|
||||
def on_bold(self, event):
|
||||
self.editor.ApplyBoldToSelection()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_italic(self, event):
|
||||
self.editor.ApplyItalicToSelection()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_underline(self, event):
|
||||
self.editor.ApplyUnderlineToSelection()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_font_size(self, event):
|
||||
size = int(self.font_size_choice.GetStringSelection())
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetFontSize(size)
|
||||
|
||||
range_obj = self.editor.GetSelectionRange()
|
||||
if range_obj.GetLength() > 0:
|
||||
self.editor.SetStyle(range_obj, attr)
|
||||
else:
|
||||
# Apply to next typed text
|
||||
self.editor.SetDefaultStyle(attr)
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_align_left(self, event):
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetAlignment(wx.TEXT_ALIGNMENT_LEFT)
|
||||
|
||||
# Get current paragraph range
|
||||
pos = self.editor.GetInsertionPoint()
|
||||
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
|
||||
if para:
|
||||
range_obj = para.GetRange()
|
||||
self.editor.SetStyle(range_obj, attr)
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_align_center(self, event):
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetAlignment(wx.TEXT_ALIGNMENT_CENTRE)
|
||||
|
||||
pos = self.editor.GetInsertionPoint()
|
||||
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
|
||||
if para:
|
||||
range_obj = para.GetRange()
|
||||
self.editor.SetStyle(range_obj, attr)
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_align_right(self, event):
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetAlignment(wx.TEXT_ALIGNMENT_RIGHT)
|
||||
|
||||
pos = self.editor.GetInsertionPoint()
|
||||
para = self.editor.GetBuffer().GetParagraphAtPosition(pos)
|
||||
if para:
|
||||
range_obj = para.GetRange()
|
||||
self.editor.SetStyle(range_obj, attr)
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_text_color(self, event):
|
||||
color_data = wx.ColourData()
|
||||
color_data.SetChooseFull(True)
|
||||
|
||||
dlg = wx.ColourDialog(self, color_data)
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
color = dlg.GetColourData().GetColour()
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetTextColour(color)
|
||||
|
||||
range_obj = self.editor.GetSelectionRange()
|
||||
if range_obj.GetLength() > 0:
|
||||
self.editor.SetStyle(range_obj, attr)
|
||||
else:
|
||||
self.editor.SetDefaultStyle(attr)
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
dlg.Destroy()
|
||||
|
||||
def on_background_color(self, event):
|
||||
color_data = wx.ColourData()
|
||||
color_data.SetChooseFull(True)
|
||||
|
||||
dlg = wx.ColourDialog(self, color_data)
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
color = dlg.GetColourData().GetColour()
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetBackgroundColour(color)
|
||||
|
||||
range_obj = self.editor.GetSelectionRange()
|
||||
if range_obj.GetLength() > 0:
|
||||
self.editor.SetStyle(range_obj, attr)
|
||||
else:
|
||||
self.editor.SetDefaultStyle(attr)
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
dlg.Destroy()
|
||||
|
||||
def on_clear_formatting(self, event):
|
||||
range_obj = self.editor.GetSelectionRange()
|
||||
if range_obj.GetLength() > 0:
|
||||
# Get the text
|
||||
text = self.editor.GetRange(range_obj.GetStart(), range_obj.GetEnd())
|
||||
|
||||
# Delete the range and reinsert as plain text
|
||||
self.editor.Delete(range_obj)
|
||||
|
||||
# Set basic style
|
||||
attr = rt.RichTextAttr()
|
||||
attr.SetFontSize(11)
|
||||
attr.SetFontFaceName("Segoe UI")
|
||||
attr.SetTextColour(wx.BLACK)
|
||||
attr.SetBackgroundColour(wx.NullColour)
|
||||
|
||||
self.editor.BeginStyle(attr)
|
||||
self.editor.WriteText(text)
|
||||
self.editor.EndStyle()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
# Menu command handlers
|
||||
def on_undo(self, event):
|
||||
if self.editor.CanUndo():
|
||||
self.editor.Undo()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_redo(self, event):
|
||||
if self.editor.CanRedo():
|
||||
self.editor.Redo()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_cut(self, event):
|
||||
self.editor.Cut()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_copy(self, event):
|
||||
self.editor.Copy()
|
||||
|
||||
def on_paste(self, event):
|
||||
self.editor.Paste()
|
||||
self.content_changed = True
|
||||
self.notes_data[self.current_note_key]['last_modified'] = time.time()
|
||||
|
||||
def on_select_all(self, event):
|
||||
self.editor.SelectAll()
|
||||
|
||||
def on_export_text(self, event):
|
||||
if not self.current_note_key:
|
||||
return
|
||||
|
||||
note_data = self.notes_data[self.current_note_key]
|
||||
title = note_data.get('title', 'note')
|
||||
|
||||
with wx.FileDialog(self, "Export note as text",
|
||||
defaultFile=f"{title}.txt",
|
||||
wildcard="Text files (*.txt)|*.txt",
|
||||
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
filename = dlg.GetPath()
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(f"{title}\n")
|
||||
f.write("=" * len(title) + "\n\n")
|
||||
f.write(self.editor.GetValue())
|
||||
wx.MessageBox("Note exported successfully!", "Success",
|
||||
wx.OK | wx.ICON_INFORMATION)
|
||||
self.update_status("Note exported to text file")
|
||||
except Exception as e:
|
||||
wx.MessageBox(f"Error exporting note: {e}", "Error",
|
||||
wx.OK | wx.ICON_ERROR)
|
||||
|
||||
def on_save_to_file(self, event):
|
||||
# Save current note first
|
||||
if self.current_note_key:
|
||||
self.auto_save_current_note()
|
||||
|
||||
with wx.FileDialog(self, "Export notes to file",
|
||||
wildcard="JSON files (*.json)|*.json",
|
||||
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
filename = dlg.GetPath()
|
||||
try:
|
||||
# Convert defaultdict to regular dict for JSON serialization
|
||||
notes_dict = {k: dict(v) for k, v in self.notes_data.items()}
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(notes_dict, f, indent=2, ensure_ascii=False)
|
||||
wx.MessageBox("Notes exported successfully!", "Success",
|
||||
wx.OK | wx.ICON_INFORMATION)
|
||||
self.update_status("All notes exported to file")
|
||||
except Exception as e:
|
||||
wx.MessageBox(f"Error saving file: {e}", "Error",
|
||||
wx.OK | wx.ICON_ERROR)
|
||||
|
||||
def on_load_from_file(self, event):
|
||||
with wx.FileDialog(self, "Import notes from file",
|
||||
wildcard="JSON files (*.json)|*.json",
|
||||
style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) as dlg:
|
||||
if dlg.ShowModal() == wx.ID_OK:
|
||||
filename = dlg.GetPath()
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
loaded_data = json.load(f)
|
||||
|
||||
# Auto-save current note before importing
|
||||
if self.current_note_key:
|
||||
self.auto_save_current_note()
|
||||
|
||||
# Merge with existing notes
|
||||
result = wx.MessageBox(
|
||||
"Replace existing notes or merge with them?\n\nYes = Replace, No = Merge",
|
||||
"Import Options",
|
||||
wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION
|
||||
)
|
||||
|
||||
if result == wx.YES: # Replace
|
||||
self.notes_data.clear()
|
||||
self.notes_data.update(loaded_data)
|
||||
self.update_status("All notes replaced with imported file")
|
||||
elif result == wx.NO: # Merge
|
||||
self.notes_data.update(loaded_data)
|
||||
self.update_status("Imported notes merged with existing notes")
|
||||
else: # Cancel
|
||||
return
|
||||
|
||||
self.load_notes_list()
|
||||
self.current_note_key = None
|
||||
self.title_ctrl.SetValue('')
|
||||
self.editor.Clear()
|
||||
self.enable_editor(False)
|
||||
wx.MessageBox("Notes imported successfully!", "Success",
|
||||
wx.OK | wx.ICON_INFORMATION)
|
||||
except Exception as e:
|
||||
wx.MessageBox(f"Error loading file: {e}", "Error",
|
||||
wx.OK | wx.ICON_ERROR)
|
||||
|
||||
def on_close(self, event):
|
||||
"""Save everything before closing"""
|
||||
self.is_closing = True
|
||||
|
||||
# Stop timers
|
||||
self.save_timer.Stop()
|
||||
self.status_timer.Stop()
|
||||
|
||||
# Save current note
|
||||
if self.current_note_key:
|
||||
self.auto_save_current_note()
|
||||
self.update_status("Final auto-save completed")
|
||||
|
||||
# Notify parent if it has a callback
|
||||
if self.parent and hasattr(self.parent, 'on_notes_closed'):
|
||||
self.parent.on_notes_closed()
|
||||
|
||||
self.Destroy()
|
||||
|
||||
def get_notes_data(self):
|
||||
"""Get all notes data"""
|
||||
# Save current note before returning
|
||||
if self.current_note_key:
|
||||
self.auto_save_current_note()
|
||||
return self.notes_data
|
||||
396
src/PrivacyNoticeDialog.py
Normal file
396
src/PrivacyNoticeDialog.py
Normal file
@@ -0,0 +1,396 @@
|
||||
import wx
|
||||
import platform
|
||||
import psutil
|
||||
import socket
|
||||
import getpass
|
||||
import subprocess
|
||||
import re
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PrivacyNoticeDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent, title="Privacy Notice", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||
self.parent = parent
|
||||
|
||||
# Calculate optimal dialog size based on screen size
|
||||
screen_width, screen_height = wx.DisplaySize()
|
||||
self.max_width = min(700, screen_width * 0.75)
|
||||
self.max_height = min(700, screen_height * 0.8)
|
||||
|
||||
self.SetMinSize((450, 400))
|
||||
self.SetSize((self.max_width, self.max_height))
|
||||
|
||||
# Create main sizer
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Create header with icon and title
|
||||
header_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
|
||||
# Add info icon
|
||||
info_icon = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX, (32, 32))
|
||||
icon_ctrl = wx.StaticBitmap(self, -1, info_icon)
|
||||
header_sizer.Add(icon_ctrl, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
|
||||
|
||||
# Add title
|
||||
title_text = wx.StaticText(self, label="Privacy and System Information")
|
||||
title_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
title_text.SetFont(title_font)
|
||||
header_sizer.Add(title_text, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
|
||||
|
||||
main_sizer.Add(header_sizer, 0, wx.EXPAND)
|
||||
|
||||
# Create scrolled window for the content
|
||||
self.scrolled_win = wx.ScrolledWindow(self)
|
||||
self.scrolled_win.SetScrollRate(10, 10)
|
||||
|
||||
scrolled_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Get system information
|
||||
system_info = self.get_system_info()
|
||||
|
||||
# Security and privacy notice section (moved to top)
|
||||
security_text = wx.StaticText(self.scrolled_win, label="Security and Privacy Notice:")
|
||||
security_font = wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
security_text.SetFont(security_font)
|
||||
scrolled_sizer.Add(security_text, 0, wx.ALL, 5)
|
||||
|
||||
# Detect potential security features
|
||||
security_warnings = self.get_security_warnings(system_info)
|
||||
|
||||
privacy_notice = (
|
||||
"wxIRC is an Open Source project and must not be distributed for commercial purposes.\n\n"
|
||||
|
||||
"Security Considerations:\n"
|
||||
f"{security_warnings}\n\n"
|
||||
|
||||
"Important Reminders:\n"
|
||||
"Your system contains hardware level security processors that operate independently\n"
|
||||
" Network traffic may be monitored by various entities\n"
|
||||
" Never discuss or plan illegal activities through any communication platform\n"
|
||||
" Keep your system and software updated for security patches\n"
|
||||
" Use strong, unique passwords and enable 2FA where available\n\n"
|
||||
|
||||
"This application collects no personal data and makes no network connections\n"
|
||||
"beyond those required for IRC functionality that you explicitly initiate."
|
||||
)
|
||||
|
||||
self.body_text = wx.StaticText(self.scrolled_win, label=privacy_notice)
|
||||
scrolled_sizer.Add(self.body_text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
# Add separator
|
||||
scrolled_sizer.Add(wx.StaticLine(self.scrolled_win), 0, wx.EXPAND | wx.ALL, 10)
|
||||
|
||||
# System information section (moved to bottom)
|
||||
sysinfo_text = wx.StaticText(self.scrolled_win, label="System Information:")
|
||||
sysinfo_text.SetFont(security_font)
|
||||
scrolled_sizer.Add(sysinfo_text, 0, wx.ALL, 5)
|
||||
|
||||
# System info details
|
||||
info_details = (
|
||||
f"Operating System: {system_info['os']}\n"
|
||||
f"Architecture: {system_info['architecture']}\n"
|
||||
f"Processor: {system_info['processor']}\n"
|
||||
f"Physical Cores: {system_info['physical_cores']}\n"
|
||||
f"Total Cores: {system_info['total_cores']}\n"
|
||||
f"Max Frequency: {system_info['max_frequency']} MHz\n"
|
||||
f"Total RAM: {system_info['total_ram']} GB\n"
|
||||
f"Available RAM: {system_info['available_ram']} GB\n"
|
||||
f"Hostname: {system_info['hostname']}\n"
|
||||
f"Username: {system_info['username']}\n"
|
||||
f"Python Version: {system_info['python_version']}\n"
|
||||
f"wxPython Version: {system_info['wx_version']}"
|
||||
)
|
||||
|
||||
info_text = wx.StaticText(self.scrolled_win, label=info_details)
|
||||
scrolled_sizer.Add(info_text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.scrolled_win.SetSizer(scrolled_sizer)
|
||||
main_sizer.Add(self.scrolled_win, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Add OK button
|
||||
ok_btn = wx.Button(self, wx.ID_OK, "I Understand and Continue")
|
||||
ok_btn.SetDefault()
|
||||
ok_btn.SetMinSize((160, 35))
|
||||
|
||||
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btn_sizer.AddStretchSpacer()
|
||||
btn_sizer.Add(ok_btn, 0, wx.ALL, 10)
|
||||
btn_sizer.AddStretchSpacer()
|
||||
|
||||
main_sizer.Add(btn_sizer, 0, wx.EXPAND)
|
||||
|
||||
self.SetSizer(main_sizer)
|
||||
|
||||
# Center on parent
|
||||
self.CentreOnParent()
|
||||
|
||||
# Bind events
|
||||
self.Bind(wx.EVT_BUTTON, self.on_ok, ok_btn)
|
||||
self.Bind(wx.EVT_SIZE, self.on_resize)
|
||||
|
||||
# Fit the dialog to content
|
||||
self.Fit()
|
||||
self.adjust_to_screen()
|
||||
|
||||
def get_system_info(self):
|
||||
"""Gather comprehensive system information with improved processor detection"""
|
||||
try:
|
||||
# CPU information
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
max_freq = int(cpu_freq.max) if cpu_freq else "N/A"
|
||||
|
||||
# Memory information
|
||||
memory = psutil.virtual_memory()
|
||||
total_ram_gb = round(memory.total / (1024**3), 1)
|
||||
available_ram_gb = round(memory.available / (1024**3), 1)
|
||||
|
||||
# Platform information
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
os_info = f"Windows {platform.release()}"
|
||||
elif system == "Linux":
|
||||
# Try to get distro info for Linux
|
||||
try:
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
if line.startswith('PRETTY_NAME='):
|
||||
os_info = line.split('=', 1)[1].strip().strip('"')
|
||||
break
|
||||
else:
|
||||
os_info = f"Linux ({platform.release()})"
|
||||
except:
|
||||
os_info = f"Linux ({platform.release()})"
|
||||
elif system == "Darwin":
|
||||
os_info = f"macOS {platform.mac_ver()[0]}"
|
||||
else:
|
||||
os_info = f"{system} {platform.release()}"
|
||||
|
||||
# Improved processor detection
|
||||
processor = self.detect_processor()
|
||||
|
||||
return {
|
||||
'os': os_info,
|
||||
'architecture': platform.architecture()[0],
|
||||
'processor': processor,
|
||||
'physical_cores': psutil.cpu_count(logical=False),
|
||||
'total_cores': psutil.cpu_count(logical=True),
|
||||
'max_frequency': max_freq,
|
||||
'total_ram': total_ram_gb,
|
||||
'available_ram': available_ram_gb,
|
||||
'hostname': socket.gethostname(),
|
||||
'username': getpass.getuser(),
|
||||
'python_version': platform.python_version(),
|
||||
'wx_version': wx.VERSION_STRING,
|
||||
'platform': platform.platform()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error gathering system info: {e}")
|
||||
return {
|
||||
'os': "Unknown",
|
||||
'architecture': "Unknown",
|
||||
'processor': "Unknown",
|
||||
'physical_cores': "N/A",
|
||||
'total_cores': "N/A",
|
||||
'max_frequency': "N/A",
|
||||
'total_ram': "N/A",
|
||||
'available_ram': "N/A",
|
||||
'hostname': "Unknown",
|
||||
'username': "Unknown",
|
||||
'python_version': platform.python_version(),
|
||||
'wx_version': wx.VERSION_STRING,
|
||||
'platform': "Unknown"
|
||||
}
|
||||
|
||||
def detect_processor(self):
|
||||
"""Improved processor detection for Ryzen and other CPUs"""
|
||||
try:
|
||||
# Method 1: Try platform.processor() first
|
||||
processor = platform.processor()
|
||||
if processor and processor.strip() and processor != "unknown":
|
||||
return processor.strip()
|
||||
|
||||
# Method 2: Try CPU info file on Linux
|
||||
if platform.system() == "Linux":
|
||||
try:
|
||||
with open('/proc/cpuinfo', 'r') as f:
|
||||
cpuinfo = f.read()
|
||||
|
||||
# Look for model name
|
||||
for line in cpuinfo.split('\n'):
|
||||
if line.startswith('model name') or line.startswith('Model Name') or line.startswith('Processor'):
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
cpu_name = parts[1].strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
|
||||
# Look for hardware field
|
||||
for line in cpuinfo.split('\n'):
|
||||
if line.startswith('Hardware'):
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
cpu_name = parts[1].strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error reading /proc/cpuinfo: {e}")
|
||||
|
||||
# Method 3: Try lscpu command
|
||||
try:
|
||||
result = subprocess.run(['lscpu'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'Model name:' in line:
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
cpu_name = parts[1].strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
except Exception as e:
|
||||
logger.debug(f"Error running lscpu: {e}")
|
||||
|
||||
# Method 4: Try sysctl on macOS
|
||||
if platform.system() == "Darwin":
|
||||
try:
|
||||
result = subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
cpu_name = result.stdout.strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
except Exception as e:
|
||||
logger.debug(f"Error running sysctl: {e}")
|
||||
|
||||
# Method 5: Try WMI on Windows
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
# Try kernel32 GetNativeSystemInfo
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
class SYSTEM_INFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wProcessorArchitecture", wintypes.WORD),
|
||||
("wReserved", wintypes.WORD),
|
||||
("dwPageSize", wintypes.DWORD),
|
||||
("lpMinimumApplicationAddress", ctypes.c_void_p),
|
||||
("lpMaximumApplicationAddress", ctypes.c_void_p),
|
||||
("dwActiveProcessorMask", ctypes.c_void_p),
|
||||
("dwNumberOfProcessors", wintypes.DWORD),
|
||||
("dwProcessorType", wintypes.DWORD),
|
||||
("dwAllocationGranularity", wintypes.DWORD),
|
||||
("wProcessorLevel", wintypes.WORD),
|
||||
("wProcessorRevision", wintypes.WORD)
|
||||
]
|
||||
|
||||
system_info = SYSTEM_INFO()
|
||||
kernel32.GetNativeSystemInfo(ctypes.byref(system_info))
|
||||
|
||||
# Map processor type to name
|
||||
processor_types = {
|
||||
586: "Pentium",
|
||||
8664: "x64",
|
||||
}
|
||||
|
||||
if system_info.dwProcessorType in processor_types:
|
||||
return processor_types[system_info.dwProcessorType]
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error with Windows processor detection: {e}")
|
||||
|
||||
# Final fallback
|
||||
return f"Unknown ({platform.machine()})"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in processor detection: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def get_security_warnings(self, system_info):
|
||||
"""Generate security warnings based on system capabilities"""
|
||||
warnings = []
|
||||
|
||||
try:
|
||||
# Check for Intel ME / AMD PSP indicators
|
||||
processor_lower = system_info['processor'].lower()
|
||||
|
||||
if 'intel' in processor_lower:
|
||||
warnings.append(" Intel Processor detected: Management Engine (ME) present")
|
||||
if 'core' in processor_lower and any(gen in processor_lower for gen in ['i3', 'i5', 'i7', 'i9']):
|
||||
warnings.append(" Modern Intel Core processor: ME capabilities active")
|
||||
|
||||
elif 'amd' in processor_lower:
|
||||
warnings.append(" AMD Processor detected: Platform Security Processor (PSP) present")
|
||||
if 'ryzen' in processor_lower:
|
||||
warnings.append(" Modern AMD Ryzen processor: PSP capabilities active")
|
||||
if '5600x' in processor_lower:
|
||||
warnings.append(" AMD Ryzen 5 5600X: Zen 3 architecture with PSP")
|
||||
|
||||
# Check for specific AMD models
|
||||
if '5600x' in processor_lower:
|
||||
warnings.append(" AMD Ryzen 5 5600X detected: Hardware level security processor active")
|
||||
|
||||
# Check RAM size
|
||||
if system_info['total_ram'] != "N/A" and system_info['total_ram'] > 8:
|
||||
warnings.append(" System has substantial RAM capacity for background processes")
|
||||
|
||||
# Check core count
|
||||
if system_info['total_cores'] != "N/A" and system_info['total_cores'] >= 4:
|
||||
warnings.append(" Multi core system capable of parallel background processing")
|
||||
|
||||
# Network capabilities
|
||||
warnings.append(" System has network connectivity capabilities")
|
||||
warnings.append(" WiFi/Ethernet hardware present for data transmission")
|
||||
|
||||
# Operating system specific
|
||||
os_lower = system_info['os'].lower()
|
||||
if 'windows' in os_lower:
|
||||
warnings.append(" Windows OS: Telemetry and background services active")
|
||||
elif 'linux' in os_lower or 'arch' in os_lower:
|
||||
warnings.append(" Linux OS: Generally more transparent, but hardware level components still active")
|
||||
if 'arch' in os_lower:
|
||||
warnings.append(" Arch Linux: Rolling release, ensure regular security updates")
|
||||
elif 'mac' in os_lower:
|
||||
warnings.append(" macOS: Apple T2/T1 security chip or Apple Silicon present")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating security warnings: {e}")
|
||||
warnings = [" Unable to fully assess system security features"]
|
||||
|
||||
return "\n".join(warnings)
|
||||
|
||||
def adjust_to_screen(self):
|
||||
"""Adjust dialog size and position to fit within screen bounds"""
|
||||
screen_width, screen_height = wx.DisplaySize()
|
||||
current_size = self.GetSize()
|
||||
|
||||
final_width = min(current_size.width, screen_width - 40)
|
||||
final_height = min(current_size.height, screen_height - 40)
|
||||
|
||||
self.SetSize(final_width, final_height)
|
||||
self.body_text.Wrap(final_width - 60)
|
||||
self.Layout()
|
||||
self.scrolled_win.FitInside()
|
||||
self.CentreOnParent()
|
||||
|
||||
def on_resize(self, event):
|
||||
"""Handle dialog resize to re-wrap text appropriately"""
|
||||
try:
|
||||
if hasattr(self, 'body_text'):
|
||||
new_width = self.GetSize().width - 60
|
||||
if new_width > 200:
|
||||
self.body_text.Wrap(new_width)
|
||||
self.scrolled_win.Layout()
|
||||
self.scrolled_win.FitInside()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in resize handler: {e}")
|
||||
event.Skip()
|
||||
|
||||
def on_ok(self, event):
|
||||
self.EndModal(wx.ID_OK)
|
||||
53
src/SearchDialog.py
Normal file
53
src/SearchDialog.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import wx
|
||||
|
||||
class SearchDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent, title="Search", style=wx.DEFAULT_DIALOG_STYLE)
|
||||
self.parent = parent
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Search input
|
||||
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
search_sizer.Add(wx.StaticText(self, label="Search:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
|
||||
self.search_ctrl = wx.TextCtrl(self, size=(200, -1), style=wx.TE_PROCESS_ENTER)
|
||||
self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
|
||||
search_sizer.Add(self.search_ctrl, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
sizer.Add(search_sizer, 0, wx.EXPAND)
|
||||
|
||||
# Options
|
||||
options_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.case_sensitive = wx.CheckBox(self, label="Case sensitive")
|
||||
self.whole_word = wx.CheckBox(self, label="Whole word")
|
||||
options_sizer.Add(self.case_sensitive, 0, wx.ALL, 5)
|
||||
options_sizer.Add(self.whole_word, 0, wx.ALL, 5)
|
||||
|
||||
sizer.Add(options_sizer, 0, wx.EXPAND)
|
||||
|
||||
# Buttons
|
||||
btn_sizer = wx.StdDialogButtonSizer()
|
||||
self.search_btn = wx.Button(self, wx.ID_OK, "Search")
|
||||
self.search_btn.SetDefault()
|
||||
self.search_btn.Bind(wx.EVT_BUTTON, self.on_search)
|
||||
btn_sizer.AddButton(self.search_btn)
|
||||
|
||||
close_btn = wx.Button(self, wx.ID_CANCEL, "Close")
|
||||
btn_sizer.AddButton(close_btn)
|
||||
btn_sizer.Realize()
|
||||
|
||||
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10)
|
||||
|
||||
self.SetSizer(sizer)
|
||||
self.Fit()
|
||||
|
||||
self.search_ctrl.SetFocus()
|
||||
|
||||
def on_search(self, event):
|
||||
search_text = self.search_ctrl.GetValue().strip()
|
||||
if search_text:
|
||||
self.parent.perform_search(
|
||||
search_text,
|
||||
self.case_sensitive.IsChecked(),
|
||||
self.whole_word.IsChecked()
|
||||
)
|
||||
BIN
src/icon.ico
Normal file
BIN
src/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
@@ -12,6 +12,11 @@ import queue
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PrivacyNoticeDialog import PrivacyNoticeDialog
|
||||
from IRCPanel import IRCPanel
|
||||
from AboutDialog import AboutDialog
|
||||
from NotesDialog import NotesDialog # Add this import
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -26,398 +31,6 @@ def get_resource_path(relative_path):
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
import platform
|
||||
import psutil
|
||||
import socket
|
||||
import getpass
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
class PrivacyNoticeDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent, title="Privacy Notice", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||
self.parent = parent
|
||||
|
||||
# Calculate optimal dialog size based on screen size
|
||||
screen_width, screen_height = wx.DisplaySize()
|
||||
self.max_width = min(700, screen_width * 0.75)
|
||||
self.max_height = min(700, screen_height * 0.8)
|
||||
|
||||
self.SetMinSize((450, 400))
|
||||
self.SetSize((self.max_width, self.max_height))
|
||||
|
||||
# Create main sizer
|
||||
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Create header with icon and title
|
||||
header_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
|
||||
# Add info icon
|
||||
info_icon = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX, (32, 32))
|
||||
icon_ctrl = wx.StaticBitmap(self, -1, info_icon)
|
||||
header_sizer.Add(icon_ctrl, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
|
||||
|
||||
# Add title
|
||||
title_text = wx.StaticText(self, label="Privacy and System Information")
|
||||
title_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
title_text.SetFont(title_font)
|
||||
header_sizer.Add(title_text, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10)
|
||||
|
||||
main_sizer.Add(header_sizer, 0, wx.EXPAND)
|
||||
|
||||
# Create scrolled window for the content
|
||||
self.scrolled_win = wx.ScrolledWindow(self)
|
||||
self.scrolled_win.SetScrollRate(10, 10)
|
||||
|
||||
scrolled_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Get system information
|
||||
system_info = self.get_system_info()
|
||||
|
||||
# Create system info section
|
||||
sysinfo_text = wx.StaticText(self.scrolled_win, label="System Information:")
|
||||
sysinfo_font = wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
sysinfo_text.SetFont(sysinfo_font)
|
||||
scrolled_sizer.Add(sysinfo_text, 0, wx.ALL, 5)
|
||||
|
||||
# System info details
|
||||
info_details = (
|
||||
f"Operating System: {system_info['os']}\n"
|
||||
f"Architecture: {system_info['architecture']}\n"
|
||||
f"Processor: {system_info['processor']}\n"
|
||||
f"Physical Cores: {system_info['physical_cores']}\n"
|
||||
f"Total Cores: {system_info['total_cores']}\n"
|
||||
f"Max Frequency: {system_info['max_frequency']} MHz\n"
|
||||
f"Total RAM: {system_info['total_ram']} GB\n"
|
||||
f"Available RAM: {system_info['available_ram']} GB\n"
|
||||
f"Hostname: {system_info['hostname']}\n"
|
||||
f"Username: {system_info['username']}\n"
|
||||
f"Python Version: {system_info['python_version']}\n"
|
||||
f"wxPython Version: {system_info['wx_version']}"
|
||||
)
|
||||
|
||||
info_text = wx.StaticText(self.scrolled_win, label=info_details)
|
||||
scrolled_sizer.Add(info_text, 0, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
# Add separator
|
||||
scrolled_sizer.Add(wx.StaticLine(self.scrolled_win), 0, wx.EXPAND | wx.ALL, 10)
|
||||
|
||||
# Security and privacy notice
|
||||
security_text = wx.StaticText(self.scrolled_win, label="Security and Privacy Notice:")
|
||||
security_text.SetFont(sysinfo_font)
|
||||
scrolled_sizer.Add(security_text, 0, wx.ALL, 5)
|
||||
|
||||
# Detect potential security features
|
||||
security_warnings = self.get_security_warnings(system_info)
|
||||
|
||||
privacy_notice = (
|
||||
"wxIRC is an Open Source project and must not be distributed for commercial purposes.\n\n"
|
||||
|
||||
"Security Considerations:\n"
|
||||
f"{security_warnings}\n\n"
|
||||
|
||||
"Important Reminders:\n"
|
||||
"Your system contains hardware level security processors that operate independently\n"
|
||||
"Network traffic may be monitored by various entities\n"
|
||||
"Never discuss or plan illegal activities through any communication platform\n"
|
||||
"Keep your system and software updated for security patches\n"
|
||||
"Use strong, unique passwords and enable 2FA where available\n\n"
|
||||
|
||||
"This application collects no personal data and makes no network connections\n"
|
||||
"beyond those required for IRC functionality that you explicitly initiate."
|
||||
)
|
||||
|
||||
self.body_text = wx.StaticText(self.scrolled_win, label=privacy_notice)
|
||||
scrolled_sizer.Add(self.body_text, 1, wx.ALL | wx.EXPAND, 10)
|
||||
|
||||
self.scrolled_win.SetSizer(scrolled_sizer)
|
||||
main_sizer.Add(self.scrolled_win, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Add OK button
|
||||
ok_btn = wx.Button(self, wx.ID_OK, "I Understand and Continue")
|
||||
ok_btn.SetDefault()
|
||||
ok_btn.SetMinSize((160, 35))
|
||||
|
||||
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btn_sizer.AddStretchSpacer()
|
||||
btn_sizer.Add(ok_btn, 0, wx.ALL, 10)
|
||||
btn_sizer.AddStretchSpacer()
|
||||
|
||||
main_sizer.Add(btn_sizer, 0, wx.EXPAND)
|
||||
|
||||
self.SetSizer(main_sizer)
|
||||
|
||||
# Center on parent
|
||||
self.CentreOnParent()
|
||||
|
||||
# Bind events
|
||||
self.Bind(wx.EVT_BUTTON, self.on_ok, ok_btn)
|
||||
self.Bind(wx.EVT_SIZE, self.on_resize)
|
||||
|
||||
# Fit the dialog to content
|
||||
self.Fit()
|
||||
self.adjust_to_screen()
|
||||
|
||||
def get_system_info(self):
|
||||
"""Gather comprehensive system information with improved processor detection"""
|
||||
try:
|
||||
# CPU information
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
max_freq = int(cpu_freq.max) if cpu_freq else "N/A"
|
||||
|
||||
# Memory information
|
||||
memory = psutil.virtual_memory()
|
||||
total_ram_gb = round(memory.total / (1024**3), 1)
|
||||
available_ram_gb = round(memory.available / (1024**3), 1)
|
||||
|
||||
# Platform information
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
os_info = f"Windows {platform.release()}"
|
||||
elif system == "Linux":
|
||||
# Try to get distro info for Linux
|
||||
try:
|
||||
with open('/etc/os-release', 'r') as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
if line.startswith('PRETTY_NAME='):
|
||||
os_info = line.split('=', 1)[1].strip().strip('"')
|
||||
break
|
||||
else:
|
||||
os_info = f"Linux ({platform.release()})"
|
||||
except:
|
||||
os_info = f"Linux ({platform.release()})"
|
||||
elif system == "Darwin":
|
||||
os_info = f"macOS {platform.mac_ver()[0]}"
|
||||
else:
|
||||
os_info = f"{system} {platform.release()}"
|
||||
|
||||
# Improved processor detection
|
||||
processor = self.detect_processor()
|
||||
|
||||
return {
|
||||
'os': os_info,
|
||||
'architecture': platform.architecture()[0],
|
||||
'processor': processor,
|
||||
'physical_cores': psutil.cpu_count(logical=False),
|
||||
'total_cores': psutil.cpu_count(logical=True),
|
||||
'max_frequency': max_freq,
|
||||
'total_ram': total_ram_gb,
|
||||
'available_ram': available_ram_gb,
|
||||
'hostname': socket.gethostname(),
|
||||
'username': getpass.getuser(),
|
||||
'python_version': platform.python_version(),
|
||||
'wx_version': wx.VERSION_STRING,
|
||||
'platform': platform.platform()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error gathering system info: {e}")
|
||||
return {
|
||||
'os': "Unknown",
|
||||
'architecture': "Unknown",
|
||||
'processor': "Unknown",
|
||||
'physical_cores': "N/A",
|
||||
'total_cores': "N/A",
|
||||
'max_frequency': "N/A",
|
||||
'total_ram': "N/A",
|
||||
'available_ram': "N/A",
|
||||
'hostname': "Unknown",
|
||||
'username': "Unknown",
|
||||
'python_version': platform.python_version(),
|
||||
'wx_version': wx.VERSION_STRING,
|
||||
'platform': "Unknown"
|
||||
}
|
||||
|
||||
def detect_processor(self):
|
||||
"""Improved processor detection for Ryzen and other CPUs"""
|
||||
try:
|
||||
# Method 1: Try platform.processor() first
|
||||
processor = platform.processor()
|
||||
if processor and processor.strip() and processor != "unknown":
|
||||
return processor.strip()
|
||||
|
||||
# Method 2: Try CPU info file on Linux
|
||||
if platform.system() == "Linux":
|
||||
try:
|
||||
with open('/proc/cpuinfo', 'r') as f:
|
||||
cpuinfo = f.read()
|
||||
|
||||
# Look for model name
|
||||
for line in cpuinfo.split('\n'):
|
||||
if line.startswith('model name') or line.startswith('Model Name') or line.startswith('Processor'):
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
cpu_name = parts[1].strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
|
||||
# Look for hardware field
|
||||
for line in cpuinfo.split('\n'):
|
||||
if line.startswith('Hardware'):
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
cpu_name = parts[1].strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error reading /proc/cpuinfo: {e}")
|
||||
|
||||
# Method 3: Try lscpu command
|
||||
try:
|
||||
result = subprocess.run(['lscpu'], capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'Model name:' in line:
|
||||
parts = line.split(':', 1)
|
||||
if len(parts) > 1:
|
||||
cpu_name = parts[1].strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
except Exception as e:
|
||||
logger.debug(f"Error running lscpu: {e}")
|
||||
|
||||
# Method 4: Try sysctl on macOS
|
||||
if platform.system() == "Darwin":
|
||||
try:
|
||||
result = subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
cpu_name = result.stdout.strip()
|
||||
if cpu_name:
|
||||
return cpu_name
|
||||
except Exception as e:
|
||||
logger.debug(f"Error running sysctl: {e}")
|
||||
|
||||
# Method 5: Try WMI on Windows
|
||||
if platform.system() == "Windows":
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
# Try kernel32 GetNativeSystemInfo
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
class SYSTEM_INFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wProcessorArchitecture", wintypes.WORD),
|
||||
("wReserved", wintypes.WORD),
|
||||
("dwPageSize", wintypes.DWORD),
|
||||
("lpMinimumApplicationAddress", ctypes.c_void_p),
|
||||
("lpMaximumApplicationAddress", ctypes.c_void_p),
|
||||
("dwActiveProcessorMask", ctypes.c_void_p),
|
||||
("dwNumberOfProcessors", wintypes.DWORD),
|
||||
("dwProcessorType", wintypes.DWORD),
|
||||
("dwAllocationGranularity", wintypes.DWORD),
|
||||
("wProcessorLevel", wintypes.WORD),
|
||||
("wProcessorRevision", wintypes.WORD)
|
||||
]
|
||||
|
||||
system_info = SYSTEM_INFO()
|
||||
kernel32.GetNativeSystemInfo(ctypes.byref(system_info))
|
||||
|
||||
# Map processor type to name
|
||||
processor_types = {
|
||||
586: "Pentium",
|
||||
8664: "x64",
|
||||
}
|
||||
|
||||
if system_info.dwProcessorType in processor_types:
|
||||
return processor_types[system_info.dwProcessorType]
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error with Windows processor detection: {e}")
|
||||
|
||||
# Final fallback
|
||||
return f"Unknown ({platform.machine()})"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in processor detection: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def get_security_warnings(self, system_info):
|
||||
"""Generate security warnings based on system capabilities"""
|
||||
warnings = []
|
||||
|
||||
try:
|
||||
# Check for Intel ME / AMD PSP indicators
|
||||
processor_lower = system_info['processor'].lower()
|
||||
|
||||
if 'intel' in processor_lower:
|
||||
warnings.append("Intel Processor detected: Management Engine (ME) present")
|
||||
if 'core' in processor_lower and any(gen in processor_lower for gen in ['i3', 'i5', 'i7', 'i9']):
|
||||
warnings.append("Modern Intel Core processor: ME capabilities active")
|
||||
|
||||
elif 'amd' in processor_lower:
|
||||
warnings.append("AMD Processor detected: Platform Security Processor (PSP) present")
|
||||
if 'ryzen' in processor_lower:
|
||||
warnings.append("Modern AMD Ryzen processor: PSP capabilities active")
|
||||
if '5600x' in processor_lower:
|
||||
warnings.append("AMD Ryzen 5 5600X: Zen 3 architecture with PSP")
|
||||
|
||||
# Check for specific AMD models
|
||||
if '5600x' in processor_lower:
|
||||
warnings.append("AMD Ryzen 5 5600X detected: Hardware level security processor active")
|
||||
|
||||
# Check RAM size
|
||||
if system_info['total_ram'] != "N/A" and system_info['total_ram'] > 8:
|
||||
warnings.append("System has substantial RAM capacity for background processes")
|
||||
|
||||
# Check core count
|
||||
if system_info['total_cores'] != "N/A" and system_info['total_cores'] >= 4:
|
||||
warnings.append("Multi core system capable of parallel background processing")
|
||||
|
||||
# Network capabilities
|
||||
warnings.append("System has network connectivity capabilities")
|
||||
warnings.append("WiFi/Ethernet hardware present for data transmission")
|
||||
|
||||
# Operating system specific
|
||||
os_lower = system_info['os'].lower()
|
||||
if 'windows' in os_lower:
|
||||
warnings.append("Windows OS: Telemetry and background services active")
|
||||
elif 'linux' in os_lower or 'arch' in os_lower:
|
||||
warnings.append("Linux OS: Generally more transparent, but hardware level components still active")
|
||||
if 'arch' in os_lower:
|
||||
warnings.append("Arch Linux: Rolling release, ensure regular security updates")
|
||||
elif 'mac' in os_lower:
|
||||
warnings.append("macOS: Apple T2/T1 security chip or Apple Silicon present")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating security warnings: {e}")
|
||||
warnings = ["Unable to fully assess system security features"]
|
||||
|
||||
return "\n".join(warnings)
|
||||
|
||||
def adjust_to_screen(self):
|
||||
"""Adjust dialog size and position to fit within screen bounds"""
|
||||
screen_width, screen_height = wx.DisplaySize()
|
||||
current_size = self.GetSize()
|
||||
|
||||
final_width = min(current_size.width, screen_width - 40)
|
||||
final_height = min(current_size.height, screen_height - 40)
|
||||
|
||||
self.SetSize(final_width, final_height)
|
||||
self.body_text.Wrap(final_width - 60)
|
||||
self.Layout()
|
||||
self.scrolled_win.FitInside()
|
||||
self.CentreOnParent()
|
||||
|
||||
def on_resize(self, event):
|
||||
"""Handle dialog resize to re-wrap text appropriately"""
|
||||
try:
|
||||
if hasattr(self, 'body_text'):
|
||||
new_width = self.GetSize().width - 60
|
||||
if new_width > 200:
|
||||
self.body_text.Wrap(new_width)
|
||||
self.scrolled_win.Layout()
|
||||
self.scrolled_win.FitInside()
|
||||
except Exception as e:
|
||||
logger.error(f"Error in resize handler: {e}")
|
||||
event.Skip()
|
||||
|
||||
def on_ok(self, event):
|
||||
self.EndModal(wx.ID_OK)
|
||||
|
||||
class UIUpdate:
|
||||
"""Thread-safe UI update container"""
|
||||
def __init__(self, callback, *args, **kwargs):
|
||||
@@ -425,454 +38,6 @@ class UIUpdate:
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
class SearchDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent, title="Search", style=wx.DEFAULT_DIALOG_STYLE)
|
||||
self.parent = parent
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Search input
|
||||
search_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
search_sizer.Add(wx.StaticText(self, label="Search:"), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
|
||||
self.search_ctrl = wx.TextCtrl(self, size=(200, -1), style=wx.TE_PROCESS_ENTER)
|
||||
self.search_ctrl.Bind(wx.EVT_TEXT_ENTER, self.on_search)
|
||||
search_sizer.Add(self.search_ctrl, 1, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
sizer.Add(search_sizer, 0, wx.EXPAND)
|
||||
|
||||
# Options
|
||||
options_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
self.case_sensitive = wx.CheckBox(self, label="Case sensitive")
|
||||
self.whole_word = wx.CheckBox(self, label="Whole word")
|
||||
options_sizer.Add(self.case_sensitive, 0, wx.ALL, 5)
|
||||
options_sizer.Add(self.whole_word, 0, wx.ALL, 5)
|
||||
|
||||
sizer.Add(options_sizer, 0, wx.EXPAND)
|
||||
|
||||
# Buttons
|
||||
btn_sizer = wx.StdDialogButtonSizer()
|
||||
self.search_btn = wx.Button(self, wx.ID_OK, "Search")
|
||||
self.search_btn.SetDefault()
|
||||
self.search_btn.Bind(wx.EVT_BUTTON, self.on_search)
|
||||
btn_sizer.AddButton(self.search_btn)
|
||||
|
||||
close_btn = wx.Button(self, wx.ID_CANCEL, "Close")
|
||||
btn_sizer.AddButton(close_btn)
|
||||
btn_sizer.Realize()
|
||||
|
||||
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10)
|
||||
|
||||
self.SetSizer(sizer)
|
||||
self.Fit()
|
||||
|
||||
self.search_ctrl.SetFocus()
|
||||
|
||||
def on_search(self, event):
|
||||
search_text = self.search_ctrl.GetValue().strip()
|
||||
if search_text:
|
||||
self.parent.perform_search(
|
||||
search_text,
|
||||
self.case_sensitive.IsChecked(),
|
||||
self.whole_word.IsChecked()
|
||||
)
|
||||
|
||||
class AboutDialog(wx.Dialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent, title="About wxIRC Client", style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
|
||||
|
||||
sizer = wx.BoxSizer(wx.VERTICAL)
|
||||
|
||||
# Application info
|
||||
info_text = wx.StaticText(self, label="wxIRC Client")
|
||||
info_font = wx.Font(14, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
info_text.SetFont(info_font)
|
||||
|
||||
version_text = wx.StaticText(self, label=f"V 1.1.1.0")
|
||||
version_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
|
||||
version_text.SetFont(version_font)
|
||||
|
||||
features_font = wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
|
||||
|
||||
# Add to sizer
|
||||
sizer.Add(info_text, 0, wx.ALL | wx.ALIGN_CENTER, 10)
|
||||
sizer.Add(version_text, 0, wx.ALL | wx.ALIGN_CENTER, 5)
|
||||
|
||||
# OK button
|
||||
ok_btn = wx.Button(self, wx.ID_OK, "OK")
|
||||
ok_btn.SetDefault()
|
||||
btn_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||
btn_sizer.Add(ok_btn, 0, wx.ALIGN_CENTER | wx.ALL, 10)
|
||||
sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER)
|
||||
|
||||
self.SetSizer(sizer)
|
||||
self.Fit()
|
||||
self.Centre()
|
||||
|
||||
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}")
|
||||
|
||||
class IRCFrame(wx.Frame):
|
||||
def __init__(self):
|
||||
super().__init__(None, title="wxIRC", size=(1200, 700))
|
||||
@@ -903,6 +68,9 @@ class IRCFrame(wx.Frame):
|
||||
self.away = False
|
||||
self.timestamps = True
|
||||
|
||||
# Notes data - Add this
|
||||
self.notes_data = defaultdict(dict)
|
||||
|
||||
# User color mapping - darker colors for white theme
|
||||
self.user_colors = {}
|
||||
self.available_colors = [
|
||||
@@ -943,7 +111,7 @@ class IRCFrame(wx.Frame):
|
||||
(wx.ACCEL_CTRL, ord('F'), wx.ID_FIND),
|
||||
(wx.ACCEL_NORMAL, wx.WXK_F3, 1001),
|
||||
(wx.ACCEL_SHIFT, wx.WXK_F3, 1002),
|
||||
(wx.ACCEL_CTRL, wx.WXK_ESCAPE, 1003), # Quick Escape
|
||||
(wx.ACCEL_SHIFT, wx.WXK_ESCAPE, 1003), # Quick Escape
|
||||
])
|
||||
self.SetAcceleratorTable(accel_tbl)
|
||||
self.Bind(wx.EVT_MENU, self.on_global_search, id=wx.ID_FIND)
|
||||
@@ -996,7 +164,7 @@ class IRCFrame(wx.Frame):
|
||||
current_panel.find_previous()
|
||||
|
||||
def on_quick_escape(self, event):
|
||||
"""Handle Ctrl+Esc for quick escape - exit immediately"""
|
||||
"""Handle Shift+Esc for quick escape - exit immediately"""
|
||||
try:
|
||||
# Stop UI timer first
|
||||
if self.ui_timer and self.ui_timer.IsRunning():
|
||||
@@ -1061,6 +229,12 @@ class IRCFrame(wx.Frame):
|
||||
self.connect_btn.Bind(wx.EVT_BUTTON, self.on_connect)
|
||||
conn_box_sizer.Add(self.connect_btn, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Add Notes button to connection box
|
||||
# self.notes_btn = wx.Button(conn_box, label="Notes")
|
||||
# self.notes_btn.SetToolTip("Open notes editor")
|
||||
# self.notes_btn.Bind(wx.EVT_BUTTON, self.on_notes)
|
||||
# conn_box_sizer.Add(self.notes_btn, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
left_sizer.Add(conn_box_sizer, 0, wx.EXPAND | wx.ALL, 5)
|
||||
|
||||
# Channel management
|
||||
@@ -1184,6 +358,7 @@ class IRCFrame(wx.Frame):
|
||||
|
||||
# Tools menu
|
||||
tools_menu = wx.Menu()
|
||||
tools_menu.Append(208, "&Notes\tCtrl+T") # Add Notes menu item
|
||||
tools_menu.Append(201, "&WHOIS User\tCtrl+I")
|
||||
tools_menu.Append(202, "Change &Nick\tCtrl+N")
|
||||
tools_menu.AppendSeparator()
|
||||
@@ -1194,6 +369,7 @@ class IRCFrame(wx.Frame):
|
||||
tools_menu.Append(205, "Set &Highlights")
|
||||
tools_menu.Append(206, "Auto-join Channels")
|
||||
tools_menu.Append(207, "Command Help")
|
||||
self.Bind(wx.EVT_MENU, self.on_notes, id=208) # Bind Notes menu item
|
||||
self.Bind(wx.EVT_MENU, self.on_menu_whois, id=201)
|
||||
self.Bind(wx.EVT_MENU, self.on_menu_change_nick, id=202)
|
||||
self.Bind(wx.EVT_MENU, self.on_menu_away, id=203)
|
||||
@@ -1220,6 +396,40 @@ class IRCFrame(wx.Frame):
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing about dialog: {e}")
|
||||
|
||||
def on_notes(self, event):
|
||||
"""Open notes editor dialog"""
|
||||
try:
|
||||
# Check if notes window already exists
|
||||
if hasattr(self, 'notes_frame') and self.notes_frame:
|
||||
try:
|
||||
self.notes_frame.Raise() # Bring to front if already open
|
||||
return
|
||||
except:
|
||||
# Frame was destroyed, create new one
|
||||
pass
|
||||
|
||||
self.notes_frame = NotesDialog(self, self.notes_data)
|
||||
self.notes_frame.Bind(wx.EVT_CLOSE, self.on_notes_closed)
|
||||
self.notes_frame.Show()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening notes dialog: {e}")
|
||||
self.log_server(f"Error opening notes: {e}", wx.Colour(255, 0, 0))
|
||||
|
||||
def on_notes_closed(self, event=None):
|
||||
"""Handle notes frame closing"""
|
||||
if hasattr(self, 'notes_frame') and self.notes_frame:
|
||||
try:
|
||||
# Update notes data from the frame before it closes
|
||||
self.notes_data = self.notes_frame.notes_data
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting notes data on close: {e}")
|
||||
finally:
|
||||
self.notes_frame = None
|
||||
|
||||
if event:
|
||||
event.Skip() # Allow the event to propagate
|
||||
|
||||
def setup_irc_handlers(self):
|
||||
try:
|
||||
self.reactor = irc.client.Reactor()
|
||||
@@ -1355,7 +565,7 @@ class IRCFrame(wx.Frame):
|
||||
self.is_connecting = False
|
||||
self.connect_btn.SetLabel("Disconnect")
|
||||
self.connect_btn.Enable(True)
|
||||
self.SetStatusText(f"Connected to {self.server} - Use /help for commands, Ctrl+F to search, Ctrl+Esc to quick exit")
|
||||
self.SetStatusText(f"Connected to {self.server} - Use /help for commands, Ctrl+F to search, Shift+Esc to quick exit")
|
||||
logger.info(f"Successfully connected to {self.server}")
|
||||
|
||||
def on_connect_failed(self, error_msg):
|
||||
@@ -1779,7 +989,7 @@ BASIC USAGE:
|
||||
- Use Up/Down arrows for message history
|
||||
- Tab for nickname completion in channels
|
||||
- Ctrl+F to search in chat history
|
||||
- Ctrl+Esc to quickly exit the application
|
||||
- Shift+Esc to quickly exit the application
|
||||
|
||||
TEXT FORMATTING:
|
||||
- Usernames are colored for easy identification
|
||||
@@ -2032,6 +1242,9 @@ COMMANDS (type /help in chat for full list):
|
||||
if self.ui_timer and self.ui_timer.IsRunning():
|
||||
self.ui_timer.Stop()
|
||||
|
||||
# Notes data will be lost when app closes (RAM only)
|
||||
# User can save to file if they want persistence
|
||||
|
||||
if self.is_connected():
|
||||
self.disconnect()
|
||||
# Give it a moment to disconnect gracefully
|
||||
@@ -2046,6 +1259,7 @@ if __name__ == "__main__":
|
||||
try:
|
||||
app = wx.App()
|
||||
frame = IRCFrame()
|
||||
frame.SetIcon(wx.Icon(get_resource_path("icon.ico"), wx.BITMAP_TYPE_ICO))
|
||||
app.MainLoop()
|
||||
except Exception as e:
|
||||
logger.critical(f"Fatal error: {e}")
|
||||
Reference in New Issue
Block a user