diff --git a/build.ps1 b/build.ps1 index 1d334dd..9c0fdb3 100644 --- a/build.ps1 +++ b/build.ps1 @@ -6,13 +6,13 @@ Write-Host "Building application with PyInstaller..." -ForegroundColor Cyan pyinstaller ` --onefile ` --noconfirm ` - --windowed ` --hidden-import wx._xml ` --add-data "FiraCode-Regular.ttf;." ` --add-data "FiraCode-SemiBold.ttf;." ` --add-data "src\sounds\*;sounds" ` --add-data "venv\Lib\site-packages\irc\codes.txt;irc" ` --add-data "icon.ico;." ` + --add-data "src\server.ico;." ` --icon "icon.ico" ` "src/main.py" diff --git a/src/IRCPanel.py b/src/IRCPanel.py index 1dc0134..166bb27 100644 --- a/src/IRCPanel.py +++ b/src/IRCPanel.py @@ -561,9 +561,13 @@ class IRCPanel(wx.Panel): logger.error(f"Error in add_message: {e}") def _add_message_safe(self, message, username_color=None, message_color=None, - bold=False, italic=False, underline=False): + bold=False, italic=False, underline=False): """Add message to display with formatting (must be called from main thread).""" try: + # Safety check: ensure text_ctrl still exists + if not self.text_ctrl or not self: + return + self.messages.append(message) # Check if user is at bottom diff --git a/src/channel.ico b/src/channel.ico new file mode 100644 index 0000000..3b78eb8 Binary files /dev/null and b/src/channel.ico differ diff --git a/src/main.py b/src/main.py index e2ce71e..ea010f0 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,6 @@ import wx +import wx.aui +import wx.lib.agw.aui as aui import irc.client import threading import re @@ -551,9 +553,18 @@ class IRCFrame(wx.Frame): left_panel.SetSizer(left_sizer) - # Center - Notebook - self.notebook = wx.Notebook(panel) + self.notebook = wx.aui.AuiNotebook(panel, style= + wx.aui.AUI_NB_DEFAULT_STYLE | + wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB | + wx.aui.AUI_NB_MIDDLE_CLICK_CLOSE + ) self.notebook.SetBackgroundColour(self.theme["content_bg"]) + + # Setup tab icons + self.setup_tab_icons() + + # Bind close event + self.notebook.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.on_notebook_page_close) # Server panel server_panel = IRCPanel(self.notebook, self) @@ -1202,11 +1213,18 @@ Available commands: try: if channel in self.channels and channel != "SERVER": def _close_channel(): + # Find and delete the page for i in range(self.notebook.GetPageCount()): if self.notebook.GetPageText(i) == channel: self.notebook.DeletePage(i) break - del self.channels[channel] + + # Clean up channel data + if channel in self.channels: + del self.channels[channel] + + if channel in self.channel_users: + del self.channel_users[channel] idx = self.channel_list.FindString(channel) if idx != wx.NOT_FOUND: @@ -1215,7 +1233,7 @@ Available commands: self.safe_ui_update(_close_channel) except Exception as e: logger.error(f"Error closing channel: {e}") - + def log_server(self, message, color=None, bold=False, italic=False, underline=False): try: if "SERVER" in self.channels: @@ -1236,8 +1254,11 @@ Available commands: def log_channel_message(self, channel, username, message, is_action=False, is_system=False): """Log a message to a channel with username coloring""" try: + # Don't create new channels if they don't exist and we're trying to log to them if channel not in self.channels: - self.safe_ui_update(self.add_channel, channel) + # Only create channel if it's being opened by the user, not just receiving messages + return + if channel in self.channels: timestamp = self.get_timestamp() @@ -1260,12 +1281,123 @@ Available commands: except Exception as e: logger.error(f"Error logging channel message: {e}") + def setup_tab_icons(self): + try: + self.tab_image_list = wx.ImageList(16, 16) + + # Possible icon locations + icon_paths = [ + os.path.join(os.path.dirname(__file__), "channel.ico"), + get_resource_path("channel.ico"), # PyInstaller bundle + os.path.join(os.getcwd(), "src", "channel.ico"), + ] + + icon_path = None + for path in icon_paths: + if path and os.path.exists(path): + icon_path = path + break + + if icon_path: + try: + img = wx.Image(icon_path, wx.BITMAP_TYPE_ICO) + img = img.Scale(16, 16, wx.IMAGE_QUALITY_HIGH) + bmp = wx.Bitmap(img) + + self.icon_server = self.tab_image_list.Add(bmp) + self.icon_channel = self.tab_image_list.Add(bmp) + self.icon_query = self.tab_image_list.Add(bmp) + + logger.info(f"Loaded tab icons from {icon_path}") + except Exception as e: + logger.warning(f"Icon load failed: {e}") + self._setup_fallback_icons() + else: + logger.info("channel.ico not found, using fallback icons") + self._setup_fallback_icons() + + self.notebook.SetImageList(self.tab_image_list) + + except Exception as e: + logger.error(f"Error setting up tab icons: {e}") + self.icon_server = -1 + self.icon_channel = -1 + self.icon_query = -1 + + def _setup_fallback_icons(self): + """Setup fallback icons using wx.ArtProvider.""" + try: + self.icon_server = self.tab_image_list.Add( + wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_OTHER, (48, 48)) + ) + self.icon_channel = self.tab_image_list.Add( + wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, (48, 48)) + ) + self.icon_query = self.tab_image_list.Add( + wx.ArtProvider.GetBitmap(wx.ART_HELP, wx.ART_OTHER, (48, 48)) + ) + except Exception as e: + logger.error(f"Error setting up fallback icons: {e}") + self.icon_server = -1 + self.icon_channel = -1 + self.icon_query = -1 + + def on_notebook_page_close(self, event): + """Handle tab close button clicks""" + try: + page_idx = event.GetSelection() + channel = self.notebook.GetPageText(page_idx) + + if channel == "Server": + event.Veto() + wx.MessageBox("Can't close Server!", "Error", wx.OK | wx.ICON_ERROR) + return + + if channel in self.channels: + del self.channels[channel] + + if channel in self.channel_users: + del self.channel_users[channel] + + # Remove from channel list + idx = self.channel_list.FindString(channel) + if idx != wx.NOT_FOUND: + self.channel_list.Delete(idx) + + if channel.startswith('#') and self.is_connected(): + try: + self.connection.part(channel) + except: + pass + + # Allow the close to proceed + event.Skip() + + except Exception as e: + logger.error(f"Error closing tab: {e}") + event.Skip() + def add_channel(self, channel): + """Add a new channel/query tab with appropriate icon""" try: if channel not in self.channels: panel = IRCPanel(self.notebook, self) panel.set_target(channel) - self.notebook.AddPage(panel, channel) + + # Determine icon based on channel type + if channel == "SERVER": + icon_idx = getattr(self, 'icon_server', -1) + elif channel.startswith('#'): + icon_idx = getattr(self, 'icon_channel', -1) + else: + icon_idx = getattr(self, 'icon_query', -1) + + # Add page with icon (if icon_idx is -1, no icon will be shown) + if icon_idx >= 0: + self.notebook.AddPage(panel, channel, select=True, imageId=icon_idx) + else: + self.notebook.AddPage(panel, channel, select=True) + self.channels[channel] = panel if channel.startswith('#'):