feat(ui): redesign settings dialog with modern GNOME HIG styling (#133)
* feat(ui): redesign settings dialog with modern GNOME HIG styling Completely redesign the settings dialog to follow GNOME Human Interface Guidelines with a modern, card-based preference page layout: UI/UX Improvements: - Add PreferencesGroup and PreferenceRow custom widgets mimicking libadwaita's AdwPreferencesPage pattern - Implement proper visual hierarchy with card-style grouped sections - Add comprehensive CSS stylesheet with theme-aware colors - Use proper spacing, border-radius, and typography following HIG - Add smooth reveal animations for settings applied notifications - Improve status indicators with semantic colors (success/warning/error) Technical Changes: - Replace flat grid layout with scrollable content area - Use ListBox rows for better accessibility and keyboard navigation - Add proper Gdk version requirement to prevent version conflicts - Consolidate model info display into a unified info card - Improve code organization with dedicated section builder methods Visual Features: - Card-like preference groups with subtle borders and shadows - Proper row hover states for interactive feedback - Status color coding: green for success, yellow for warnings, red for errors - Cleaner legend display for model download status - Modern flat button styling for auxiliary actions All 328 existing tests continue to pass. * refactor(settings): simplify to Close-only button pattern Remove Apply/Cancel buttons in favor of pure instant-apply UX: - Settings now apply immediately when changed (auto-apply) - Single Close button follows modern GNOME Settings pattern - Remove redundant _revert_settings() and _show_settings_applied_message() - Update docstring to reflect instant-apply design This resolves the UX inconsistency where settings auto-applied but also had Apply/Cancel buttons that were confusing. * feat(ui): redesign Logs and About dialogs with GNOME HIG styling Logs Dialog: - Modern card-based filter bar with grouped controls - Theme-aware log level colors (works in light/dark mode) - Toolbar with icon buttons for Copy, Export, Clear actions - Toast notifications for user feedback - Close-only button pattern (consistent with settings) About Dialog: - Extract to separate about_dialog.py module - Use standard Gtk.AboutDialog (GNOME recommended) - Clean credits section with contributors and tech stack - Remove emojis for cleaner professional look - Better organized credit sections Both dialogs now follow the same design language as the Settings dialog redesign.
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
About Dialog for Vocalinux.
|
||||
|
||||
A modern, GNOME HIG-compliant about dialog with:
|
||||
- Clean card-based layout
|
||||
- App icon and branding
|
||||
- Version and description
|
||||
- Credits section with tabs
|
||||
- Links to GitHub and license
|
||||
|
||||
UX Design Notes:
|
||||
- Follows GNOME Human Interface Guidelines
|
||||
- Uses standard Gtk.AboutDialog as base (GNOME apps do this)
|
||||
- Customizes with better styling and organization
|
||||
- Close-only button pattern
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import GdkPixbuf, Gtk # noqa: E402
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def show_about_dialog(parent: Gtk.Window = None):
|
||||
"""
|
||||
Show the About dialog for Vocalinux.
|
||||
|
||||
Uses Gtk.AboutDialog which is the GNOME-standard way to show app info.
|
||||
This is the recommended approach per GNOME HIG.
|
||||
|
||||
Args:
|
||||
parent: Parent window for the dialog
|
||||
"""
|
||||
from ..utils.resource_manager import ResourceManager
|
||||
from ..version import __copyright__, __description__, __url__, __version__
|
||||
|
||||
resource_manager = ResourceManager()
|
||||
|
||||
about_dialog = Gtk.AboutDialog()
|
||||
about_dialog.set_transient_for(parent)
|
||||
about_dialog.set_modal(True)
|
||||
|
||||
# Basic info
|
||||
about_dialog.set_program_name("Vocalinux")
|
||||
about_dialog.set_version(__version__)
|
||||
about_dialog.set_copyright(__copyright__)
|
||||
|
||||
# Description with cleaner formatting (no emojis in main comments)
|
||||
about_dialog.set_comments(__description__)
|
||||
|
||||
# Website
|
||||
about_dialog.set_website(__url__)
|
||||
about_dialog.set_website_label("Star on GitHub")
|
||||
|
||||
# License
|
||||
about_dialog.set_license_type(Gtk.License.GPL_3_0)
|
||||
|
||||
# Credits
|
||||
about_dialog.set_authors(["Jatin K Malik <jatinkrmalik@gmail.com>"])
|
||||
about_dialog.set_documenters(["Jatin K Malik"])
|
||||
about_dialog.set_artists(["Jatin K Malik"])
|
||||
|
||||
# Additional credits section
|
||||
about_dialog.add_credit_section(
|
||||
"Contributors",
|
||||
[
|
||||
"Open to contributions!",
|
||||
"github.com/jatinkrmalik/vocalinux",
|
||||
],
|
||||
)
|
||||
|
||||
about_dialog.add_credit_section(
|
||||
"Built With",
|
||||
[
|
||||
"VOSK Speech Recognition",
|
||||
"OpenAI Whisper",
|
||||
"GTK 3 / PyGObject",
|
||||
],
|
||||
)
|
||||
|
||||
# Set the logo
|
||||
logo_path = resource_manager.get_icon_path("vocalinux")
|
||||
if os.path.exists(logo_path):
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(logo_path)
|
||||
# Scale to appropriate size for about dialog
|
||||
scaled_pixbuf = pixbuf.scale_simple(128, 128, GdkPixbuf.InterpType.BILINEAR)
|
||||
about_dialog.set_logo(scaled_pixbuf)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load or scale logo: {e}")
|
||||
|
||||
# Run and clean up
|
||||
about_dialog.run()
|
||||
about_dialog.destroy()
|
||||
|
||||
|
||||
class AboutDialog(Gtk.Dialog):
|
||||
"""
|
||||
Custom About Dialog with modern GNOME styling.
|
||||
|
||||
This is an alternative to Gtk.AboutDialog for apps that want more
|
||||
customization. Uses card-based layout with tabs for different sections.
|
||||
"""
|
||||
|
||||
# CSS for modern styling
|
||||
ABOUT_CSS = """
|
||||
.about-dialog {
|
||||
background-color: @theme_bg_color;
|
||||
}
|
||||
|
||||
.about-header {
|
||||
padding: 32px 24px 24px 24px;
|
||||
}
|
||||
|
||||
.about-icon {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.about-title {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.about-version {
|
||||
font-size: 0.9em;
|
||||
color: @theme_unfocused_fg_color;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.about-description {
|
||||
color: @theme_unfocused_fg_color;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.about-links {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.about-link-button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background-color: @theme_selected_bg_color;
|
||||
color: @theme_selected_fg_color;
|
||||
}
|
||||
|
||||
.about-link-button:hover {
|
||||
background-color: shade(@theme_selected_bg_color, 1.1);
|
||||
}
|
||||
|
||||
.about-section {
|
||||
background-color: @theme_base_color;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin: 8px 24px;
|
||||
border: 1px solid alpha(@borders, 0.5);
|
||||
}
|
||||
|
||||
.about-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85em;
|
||||
color: @theme_unfocused_fg_color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.about-credit {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.about-credit-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.about-credit-role {
|
||||
font-size: 0.85em;
|
||||
color: @theme_unfocused_fg_color;
|
||||
}
|
||||
|
||||
.about-footer {
|
||||
padding: 16px 24px;
|
||||
color: @theme_unfocused_fg_color;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, parent: Gtk.Window = None):
|
||||
from ..utils.resource_manager import ResourceManager
|
||||
from ..version import __copyright__, __description__, __url__, __version__
|
||||
|
||||
super().__init__(
|
||||
title="About Vocalinux",
|
||||
transient_for=parent,
|
||||
flags=Gtk.DialogFlags.MODAL,
|
||||
)
|
||||
|
||||
self.resource_manager = ResourceManager()
|
||||
self.version = __version__
|
||||
self.description = __description__
|
||||
self.url = __url__
|
||||
self.copyright = __copyright__
|
||||
|
||||
# Setup
|
||||
self._setup_css()
|
||||
self.set_default_size(400, -1)
|
||||
self.set_resizable(False)
|
||||
self.get_style_context().add_class("about-dialog")
|
||||
|
||||
# Only Close button
|
||||
self.add_button("_Close", Gtk.ResponseType.CLOSE)
|
||||
|
||||
# Build UI
|
||||
self._build_ui()
|
||||
|
||||
# Connect response
|
||||
self.connect("response", lambda d, r: d.destroy())
|
||||
|
||||
self.show_all()
|
||||
|
||||
def _setup_css(self):
|
||||
"""Set up CSS styling."""
|
||||
from gi.repository import Gdk
|
||||
|
||||
css_provider = Gtk.CssProvider()
|
||||
css_provider.load_from_data(self.ABOUT_CSS.encode())
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
Gdk.Screen.get_default(),
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
def _build_ui(self):
|
||||
"""Build the dialog UI."""
|
||||
content_area = self.get_content_area()
|
||||
|
||||
# Main container
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
content_area.add(main_box)
|
||||
|
||||
# Header section (icon, name, version, description)
|
||||
header = self._build_header()
|
||||
main_box.pack_start(header, False, False, 0)
|
||||
|
||||
# Links section
|
||||
links = self._build_links()
|
||||
main_box.pack_start(links, False, False, 0)
|
||||
|
||||
# Credits section
|
||||
credits_section = self._build_credits()
|
||||
main_box.pack_start(credits_section, False, False, 0)
|
||||
|
||||
# Footer (copyright, license)
|
||||
footer = self._build_footer()
|
||||
main_box.pack_start(footer, False, False, 0)
|
||||
|
||||
def _build_header(self):
|
||||
"""Build the header with icon, name, version."""
|
||||
header = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
header.get_style_context().add_class("about-header")
|
||||
header.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
# App icon
|
||||
logo_path = self.resource_manager.get_icon_path("vocalinux")
|
||||
if os.path.exists(logo_path):
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(logo_path)
|
||||
scaled = pixbuf.scale_simple(96, 96, GdkPixbuf.InterpType.BILINEAR)
|
||||
icon = Gtk.Image.new_from_pixbuf(scaled)
|
||||
icon.get_style_context().add_class("about-icon")
|
||||
header.pack_start(icon, False, False, 0)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load logo: {e}")
|
||||
|
||||
# App name
|
||||
name_label = Gtk.Label(label="Vocalinux")
|
||||
name_label.get_style_context().add_class("about-title")
|
||||
header.pack_start(name_label, False, False, 0)
|
||||
|
||||
# Version
|
||||
version_label = Gtk.Label(label=self.version)
|
||||
version_label.get_style_context().add_class("about-version")
|
||||
header.pack_start(version_label, False, False, 0)
|
||||
|
||||
# Description
|
||||
desc_label = Gtk.Label(label=self.description)
|
||||
desc_label.get_style_context().add_class("about-description")
|
||||
desc_label.set_line_wrap(True)
|
||||
desc_label.set_max_width_chars(40)
|
||||
desc_label.set_justify(Gtk.Justification.CENTER)
|
||||
header.pack_start(desc_label, False, False, 0)
|
||||
|
||||
return header
|
||||
|
||||
def _build_links(self):
|
||||
"""Build the links section."""
|
||||
links_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
links_box.set_halign(Gtk.Align.CENTER)
|
||||
links_box.get_style_context().add_class("about-links")
|
||||
links_box.set_margin_bottom(16)
|
||||
|
||||
# GitHub button
|
||||
github_button = Gtk.LinkButton.new_with_label(self.url, "Star on GitHub")
|
||||
github_button.get_style_context().add_class("about-link-button")
|
||||
links_box.pack_start(github_button, False, False, 0)
|
||||
|
||||
return links_box
|
||||
|
||||
def _build_credits(self):
|
||||
"""Build the credits section."""
|
||||
section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
section.get_style_context().add_class("about-section")
|
||||
|
||||
# Title
|
||||
title = Gtk.Label(label="CREDITS", xalign=0)
|
||||
title.get_style_context().add_class("about-section-title")
|
||||
section.pack_start(title, False, False, 0)
|
||||
|
||||
# Author
|
||||
author_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
||||
author_box.get_style_context().add_class("about-credit")
|
||||
|
||||
author_name = Gtk.Label(label="Jatin K Malik", xalign=0)
|
||||
author_name.get_style_context().add_class("about-credit-name")
|
||||
author_box.pack_start(author_name, False, False, 0)
|
||||
|
||||
author_role = Gtk.Label(label="Author & Maintainer", xalign=0)
|
||||
author_role.get_style_context().add_class("about-credit-role")
|
||||
author_box.pack_start(author_role, False, False, 0)
|
||||
|
||||
section.pack_start(author_box, False, False, 0)
|
||||
|
||||
# Technologies
|
||||
tech_label = Gtk.Label(xalign=0)
|
||||
tech_label.set_markup("<small>Built with VOSK, OpenAI Whisper, GTK 3, and Python</small>")
|
||||
tech_label.get_style_context().add_class("about-credit-role")
|
||||
tech_label.set_margin_top(8)
|
||||
section.pack_start(tech_label, False, False, 0)
|
||||
|
||||
return section
|
||||
|
||||
def _build_footer(self):
|
||||
"""Build the footer with copyright and license."""
|
||||
footer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
footer.get_style_context().add_class("about-footer")
|
||||
footer.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
# Copyright
|
||||
copyright_label = Gtk.Label(label=self.copyright)
|
||||
copyright_label.set_line_wrap(True)
|
||||
footer.pack_start(copyright_label, False, False, 0)
|
||||
|
||||
# License
|
||||
license_label = Gtk.Label()
|
||||
license_label.set_markup(
|
||||
'<a href="https://www.gnu.org/licenses/gpl-3.0.html">'
|
||||
"GNU General Public License v3.0</a>"
|
||||
)
|
||||
footer.pack_start(license_label, False, False, 0)
|
||||
|
||||
return footer
|
||||
@@ -3,6 +3,12 @@ Logging viewer dialog for VocaLinux.
|
||||
|
||||
This module provides a GTK dialog for viewing, filtering, and exporting
|
||||
application logs for debugging purposes.
|
||||
|
||||
UX Design Notes:
|
||||
- Follows GNOME Human Interface Guidelines for modern desktop look
|
||||
- Uses card-based layout with grouped filter controls
|
||||
- Theme-aware colors for log levels (works in light/dark mode)
|
||||
- Close-only button pattern (instant-apply for filters)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -12,19 +18,174 @@ from typing import Optional
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import GLib, Gtk # noqa: E402
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk, GLib, Gtk, Pango # noqa: E402
|
||||
|
||||
from .logging_manager import LogRecord, get_logging_manager # noqa: E402
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# CSS for modern styling
|
||||
LOGGING_CSS = """
|
||||
/* Modern GNOME-style logging dialog */
|
||||
.logging-dialog {
|
||||
background-color: @theme_bg_color;
|
||||
}
|
||||
|
||||
/* Filter bar styling - card-like appearance */
|
||||
.filter-bar {
|
||||
background-color: @theme_base_color;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid alpha(@borders, 0.5);
|
||||
}
|
||||
|
||||
.filter-bar label {
|
||||
color: @theme_unfocused_fg_color;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.filter-bar entry {
|
||||
min-height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.filter-bar combobox button {
|
||||
min-height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Log view styling */
|
||||
.log-view-container {
|
||||
background-color: @theme_base_color;
|
||||
border-radius: 12px;
|
||||
border: 1px solid alpha(@borders, 0.5);
|
||||
}
|
||||
|
||||
.log-view {
|
||||
font-family: "JetBrains Mono", "Fira Code", "Source Code Pro", monospace;
|
||||
font-size: 9pt;
|
||||
padding: 12px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Status bar styling */
|
||||
.status-bar {
|
||||
background-color: @theme_base_color;
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid alpha(@borders, 0.3);
|
||||
}
|
||||
|
||||
.status-bar label {
|
||||
font-size: 0.85em;
|
||||
color: @theme_unfocused_fg_color;
|
||||
}
|
||||
|
||||
.status-count {
|
||||
font-weight: 500;
|
||||
color: @theme_fg_color;
|
||||
}
|
||||
|
||||
/* Level indicators */
|
||||
.level-debug {
|
||||
color: alpha(@theme_fg_color, 0.6);
|
||||
}
|
||||
|
||||
.level-info {
|
||||
color: @theme_fg_color;
|
||||
}
|
||||
|
||||
.level-warning {
|
||||
color: #e5a50a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.level-error {
|
||||
color: #c01c28;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.level-critical {
|
||||
color: #ffffff;
|
||||
background-color: #c01c28;
|
||||
font-weight: 700;
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
/* Action buttons in header */
|
||||
.header-button {
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.header-button.destructive {
|
||||
color: #c01c28;
|
||||
}
|
||||
|
||||
.header-button.destructive:hover {
|
||||
background-color: alpha(#c01c28, 0.1);
|
||||
}
|
||||
|
||||
/* Toolbar styling */
|
||||
.toolbar-box {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.toolbar-box button {
|
||||
border-radius: 6px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.toolbar-box button.suggested-action {
|
||||
background-color: @theme_selected_bg_color;
|
||||
color: @theme_selected_fg_color;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
opacity: 0.4;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.empty-state-subtitle {
|
||||
color: @theme_unfocused_fg_color;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _setup_css():
|
||||
"""Set up CSS styling for the logging dialog."""
|
||||
css_provider = Gtk.CssProvider()
|
||||
css_provider.load_from_data(LOGGING_CSS.encode())
|
||||
Gtk.StyleContext.add_provider_for_screen(
|
||||
Gdk.Screen.get_default(),
|
||||
css_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
)
|
||||
|
||||
|
||||
class LoggingDialog(Gtk.Dialog):
|
||||
"""GTK Dialog for viewing and managing application logs."""
|
||||
"""Modern GTK Dialog for viewing and managing application logs."""
|
||||
|
||||
def __init__(self, parent: Optional[Gtk.Window] = None):
|
||||
super().__init__(
|
||||
title="VocaLinux Logs",
|
||||
title="Logs",
|
||||
transient_for=parent,
|
||||
flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
modal=False,
|
||||
@@ -35,15 +196,15 @@ class LoggingDialog(Gtk.Dialog):
|
||||
self.filter_level = None
|
||||
self.filter_module = None
|
||||
|
||||
# Setup CSS styling
|
||||
_setup_css()
|
||||
|
||||
# Set dialog properties
|
||||
self.set_default_size(900, 700)
|
||||
self.set_border_width(10)
|
||||
self.get_style_context().add_class("logging-dialog")
|
||||
|
||||
# Add buttons
|
||||
self.add_button("Close", Gtk.ResponseType.CLOSE)
|
||||
self.add_button("Copy All", Gtk.ResponseType.HELP)
|
||||
self.add_button("Export", Gtk.ResponseType.APPLY)
|
||||
self.add_button("Clear", Gtk.ResponseType.REJECT)
|
||||
# Add only Close button (actions are in header bar style)
|
||||
self.add_button("_Close", Gtk.ResponseType.CLOSE)
|
||||
|
||||
# Create UI
|
||||
self._create_ui()
|
||||
@@ -61,14 +222,22 @@ class LoggingDialog(Gtk.Dialog):
|
||||
def _create_ui(self):
|
||||
"""Create the user interface."""
|
||||
content_area = self.get_content_area()
|
||||
content_area.set_margin_top(16)
|
||||
content_area.set_margin_bottom(8)
|
||||
content_area.set_margin_start(16)
|
||||
content_area.set_margin_end(16)
|
||||
|
||||
# Create main container
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
content_area.add(main_box)
|
||||
|
||||
# Create toolbar
|
||||
toolbar_box = self._create_toolbar()
|
||||
main_box.pack_start(toolbar_box, False, False, 0)
|
||||
# Create filter bar (card-style)
|
||||
filter_bar = self._create_filter_bar()
|
||||
main_box.pack_start(filter_bar, False, False, 0)
|
||||
|
||||
# Create action toolbar
|
||||
toolbar = self._create_toolbar()
|
||||
main_box.pack_start(toolbar, False, False, 0)
|
||||
|
||||
# Create log view (give it most of the space)
|
||||
log_view_box = self._create_log_view()
|
||||
@@ -81,13 +250,32 @@ class LoggingDialog(Gtk.Dialog):
|
||||
# Show all widgets
|
||||
main_box.show_all()
|
||||
|
||||
def _create_toolbar(self):
|
||||
"""Create the toolbar with filters and controls."""
|
||||
toolbar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
def _create_filter_bar(self):
|
||||
"""Create the filter bar with modern card styling."""
|
||||
filter_frame = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||
filter_frame.get_style_context().add_class("filter-bar")
|
||||
|
||||
# Title row
|
||||
title_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
title_label = Gtk.Label(label="Filters", xalign=0)
|
||||
title_label.set_markup("<b>Filters</b>")
|
||||
title_box.pack_start(title_label, False, False, 0)
|
||||
|
||||
# Auto-scroll toggle (moved to right of title)
|
||||
self.auto_scroll_check = Gtk.CheckButton(label="Auto-scroll")
|
||||
self.auto_scroll_check.set_active(True)
|
||||
self.auto_scroll_check.connect("toggled", self._on_auto_scroll_toggled)
|
||||
title_box.pack_end(self.auto_scroll_check, False, False, 0)
|
||||
|
||||
filter_frame.pack_start(title_box, False, False, 0)
|
||||
|
||||
# Filter controls row
|
||||
controls_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||
|
||||
# Level filter
|
||||
level_label = Gtk.Label(label="Level:")
|
||||
toolbar_box.pack_start(level_label, False, False, 0)
|
||||
level_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
level_label = Gtk.Label(label="Level")
|
||||
level_box.pack_start(level_label, False, False, 0)
|
||||
|
||||
self.level_combo = Gtk.ComboBoxText()
|
||||
self.level_combo.append("ALL", "All Levels")
|
||||
@@ -98,37 +286,98 @@ class LoggingDialog(Gtk.Dialog):
|
||||
self.level_combo.append("CRITICAL", "Critical")
|
||||
self.level_combo.set_active(0)
|
||||
self.level_combo.connect("changed", self._on_filter_changed)
|
||||
toolbar_box.pack_start(self.level_combo, False, False, 0)
|
||||
level_box.pack_start(self.level_combo, False, False, 0)
|
||||
|
||||
controls_box.pack_start(level_box, False, False, 0)
|
||||
|
||||
# Separator
|
||||
sep = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
|
||||
controls_box.pack_start(sep, False, False, 4)
|
||||
|
||||
# Module filter
|
||||
module_label = Gtk.Label(label="Module:")
|
||||
toolbar_box.pack_start(module_label, False, False, 0)
|
||||
module_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
module_label = Gtk.Label(label="Module")
|
||||
module_box.pack_start(module_label, False, False, 0)
|
||||
|
||||
self.module_entry = Gtk.Entry()
|
||||
self.module_entry.set_placeholder_text("Filter by module name...")
|
||||
self.module_entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.PRIMARY, "edit-find-symbolic"
|
||||
)
|
||||
self.module_entry.connect("changed", self._on_filter_changed)
|
||||
toolbar_box.pack_start(self.module_entry, True, True, 0)
|
||||
module_box.pack_start(self.module_entry, True, True, 0)
|
||||
|
||||
# Auto-scroll toggle
|
||||
self.auto_scroll_check = Gtk.CheckButton(label="Auto-scroll")
|
||||
self.auto_scroll_check.set_active(True)
|
||||
self.auto_scroll_check.connect("toggled", self._on_auto_scroll_toggled)
|
||||
toolbar_box.pack_start(self.auto_scroll_check, False, False, 0)
|
||||
controls_box.pack_start(module_box, True, True, 0)
|
||||
|
||||
filter_frame.pack_start(controls_box, False, False, 0)
|
||||
|
||||
return filter_frame
|
||||
|
||||
def _create_toolbar(self):
|
||||
"""Create the action toolbar."""
|
||||
toolbar_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
toolbar_box.get_style_context().add_class("toolbar-box")
|
||||
|
||||
# Refresh button
|
||||
refresh_button = Gtk.Button(label="Refresh")
|
||||
refresh_button = Gtk.Button()
|
||||
refresh_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
refresh_icon = Gtk.Image.new_from_icon_name("view-refresh-symbolic", Gtk.IconSize.BUTTON)
|
||||
refresh_box.pack_start(refresh_icon, False, False, 0)
|
||||
refresh_box.pack_start(Gtk.Label(label="Refresh"), False, False, 0)
|
||||
refresh_button.add(refresh_box)
|
||||
refresh_button.set_tooltip_text("Refresh log view")
|
||||
refresh_button.connect("clicked", self._on_refresh_clicked)
|
||||
toolbar_box.pack_start(refresh_button, False, False, 0)
|
||||
|
||||
# Spacer
|
||||
toolbar_box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
# Copy button
|
||||
copy_button = Gtk.Button()
|
||||
copy_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
copy_icon = Gtk.Image.new_from_icon_name("edit-copy-symbolic", Gtk.IconSize.BUTTON)
|
||||
copy_box.pack_start(copy_icon, False, False, 0)
|
||||
copy_box.pack_start(Gtk.Label(label="Copy"), False, False, 0)
|
||||
copy_button.add(copy_box)
|
||||
copy_button.set_tooltip_text("Copy all logs to clipboard")
|
||||
copy_button.connect("clicked", lambda w: self._copy_logs_to_clipboard())
|
||||
toolbar_box.pack_start(copy_button, False, False, 0)
|
||||
|
||||
# Export button
|
||||
export_button = Gtk.Button()
|
||||
export_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
export_icon = Gtk.Image.new_from_icon_name("document-save-symbolic", Gtk.IconSize.BUTTON)
|
||||
export_box.pack_start(export_icon, False, False, 0)
|
||||
export_box.pack_start(Gtk.Label(label="Export"), False, False, 0)
|
||||
export_button.add(export_box)
|
||||
export_button.set_tooltip_text("Export logs to file")
|
||||
export_button.connect("clicked", lambda w: self._export_logs())
|
||||
toolbar_box.pack_start(export_button, False, False, 0)
|
||||
|
||||
# Clear button (destructive action)
|
||||
clear_button = Gtk.Button()
|
||||
clear_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
clear_icon = Gtk.Image.new_from_icon_name("user-trash-symbolic", Gtk.IconSize.BUTTON)
|
||||
clear_box.pack_start(clear_icon, False, False, 0)
|
||||
clear_box.pack_start(Gtk.Label(label="Clear"), False, False, 0)
|
||||
clear_button.add(clear_box)
|
||||
clear_button.get_style_context().add_class("header-button")
|
||||
clear_button.get_style_context().add_class("destructive")
|
||||
clear_button.set_tooltip_text("Clear all logs")
|
||||
clear_button.connect("clicked", lambda w: self._clear_logs())
|
||||
toolbar_box.pack_start(clear_button, False, False, 0)
|
||||
|
||||
return toolbar_box
|
||||
|
||||
def _create_log_view(self):
|
||||
"""Create the main log viewing area."""
|
||||
# Container with rounded corners
|
||||
container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
container.get_style_context().add_class("log-view-container")
|
||||
|
||||
# Create scrolled window
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
scrolled.set_shadow_type(Gtk.ShadowType.IN)
|
||||
# Set minimum height to ensure the text view takes up most of the dialog space
|
||||
scrolled.set_min_content_height(400)
|
||||
scrolled.set_vexpand(True)
|
||||
scrolled.set_hexpand(True)
|
||||
@@ -137,62 +386,93 @@ class LoggingDialog(Gtk.Dialog):
|
||||
self.text_view = Gtk.TextView()
|
||||
self.text_view.set_editable(False)
|
||||
self.text_view.set_cursor_visible(False)
|
||||
self.text_view.set_wrap_mode(Gtk.WrapMode.WORD)
|
||||
# Ensure the text view expands to fill available space
|
||||
self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||
self.text_view.set_vexpand(True)
|
||||
self.text_view.set_hexpand(True)
|
||||
|
||||
# Set monospace font
|
||||
font_desc = self.text_view.get_pango_context().get_font_description()
|
||||
font_desc.set_family("monospace")
|
||||
font_desc.set_size(9 * 1024) # 9pt
|
||||
self.text_view.override_font(font_desc)
|
||||
self.text_view.set_left_margin(12)
|
||||
self.text_view.set_right_margin(12)
|
||||
self.text_view.set_top_margin(12)
|
||||
self.text_view.set_bottom_margin(12)
|
||||
self.text_view.get_style_context().add_class("log-view")
|
||||
|
||||
# Get text buffer
|
||||
self.text_buffer = self.text_view.get_buffer()
|
||||
|
||||
# Create text tags for different log levels
|
||||
# Create text tags for different log levels (theme-aware)
|
||||
self._create_text_tags()
|
||||
|
||||
scrolled.add(self.text_view)
|
||||
container.pack_start(scrolled, True, True, 0)
|
||||
|
||||
# Store reference to scrolled window for auto-scrolling
|
||||
self.scrolled_window = scrolled
|
||||
|
||||
return scrolled
|
||||
return container
|
||||
|
||||
def _create_text_tags(self):
|
||||
"""Create text tags for different log levels."""
|
||||
# Debug - gray
|
||||
"""Create text tags for different log levels with theme-aware colors."""
|
||||
# Get the style context for theme colors
|
||||
style_context = self.text_view.get_style_context()
|
||||
|
||||
# Debug - muted/gray (theme-aware)
|
||||
debug_tag = self.text_buffer.create_tag("DEBUG")
|
||||
debug_tag.set_property("foreground", "#666666")
|
||||
debug_tag.set_property("foreground", "#888888")
|
||||
debug_tag.set_property("style", Pango.Style.ITALIC)
|
||||
|
||||
# Info - default color
|
||||
self.text_buffer.create_tag("INFO")
|
||||
# Info - default foreground (theme-aware)
|
||||
info_tag = self.text_buffer.create_tag("INFO")
|
||||
# No foreground set - uses default theme color
|
||||
|
||||
# Warning - orange
|
||||
# Warning - orange (visible in both light/dark)
|
||||
warning_tag = self.text_buffer.create_tag("WARNING")
|
||||
warning_tag.set_property("foreground", "#FF8C00")
|
||||
warning_tag.set_property("weight", 600)
|
||||
warning_tag.set_property("foreground", "#e5a50a")
|
||||
warning_tag.set_property("weight", Pango.Weight.SEMIBOLD)
|
||||
|
||||
# Error - red
|
||||
# Error - red (visible in both light/dark)
|
||||
error_tag = self.text_buffer.create_tag("ERROR")
|
||||
error_tag.set_property("foreground", "#DC143C")
|
||||
error_tag.set_property("weight", 700)
|
||||
error_tag.set_property("foreground", "#e01b24")
|
||||
error_tag.set_property("weight", Pango.Weight.BOLD)
|
||||
|
||||
# Critical - red background
|
||||
# Critical - white on red background
|
||||
critical_tag = self.text_buffer.create_tag("CRITICAL")
|
||||
critical_tag.set_property("foreground", "#FFFFFF")
|
||||
critical_tag.set_property("background", "#DC143C")
|
||||
critical_tag.set_property("weight", 700)
|
||||
critical_tag.set_property("foreground", "#ffffff")
|
||||
critical_tag.set_property("background", "#c01c28")
|
||||
critical_tag.set_property("weight", Pango.Weight.BOLD)
|
||||
|
||||
# Timestamp tag - subtle
|
||||
timestamp_tag = self.text_buffer.create_tag("timestamp")
|
||||
timestamp_tag.set_property("foreground", "#888888")
|
||||
timestamp_tag.set_property("scale", 0.95)
|
||||
|
||||
# Module name tag - slightly emphasized
|
||||
module_tag = self.text_buffer.create_tag("module")
|
||||
module_tag.set_property("weight", Pango.Weight.MEDIUM)
|
||||
|
||||
def _create_status_bar(self):
|
||||
"""Create the status bar."""
|
||||
status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
"""Create the status bar with statistics."""
|
||||
status_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=16)
|
||||
status_box.get_style_context().add_class("status-bar")
|
||||
|
||||
self.status_label = Gtk.Label()
|
||||
self.status_label.set_halign(Gtk.Align.START)
|
||||
status_box.pack_start(self.status_label, True, True, 0)
|
||||
# Left side - total count
|
||||
self.total_label = Gtk.Label()
|
||||
self.total_label.get_style_context().add_class("status-count")
|
||||
status_box.pack_start(self.total_label, False, False, 0)
|
||||
|
||||
# Separator
|
||||
sep = Gtk.Separator(orientation=Gtk.Orientation.VERTICAL)
|
||||
status_box.pack_start(sep, False, False, 0)
|
||||
|
||||
# Level breakdown
|
||||
self.level_stats_label = Gtk.Label()
|
||||
status_box.pack_start(self.level_stats_label, False, False, 0)
|
||||
|
||||
# Spacer
|
||||
status_box.pack_start(Gtk.Box(), True, True, 0)
|
||||
|
||||
# Memory indicator (optional info)
|
||||
self.memory_label = Gtk.Label()
|
||||
self.memory_label.set_markup("<small>Buffer: --</small>")
|
||||
status_box.pack_end(self.memory_label, False, False, 0)
|
||||
|
||||
# Update status
|
||||
self._update_status()
|
||||
@@ -203,18 +483,37 @@ class LoggingDialog(Gtk.Dialog):
|
||||
"""Update the status bar with current log statistics."""
|
||||
stats = self.logging_manager.get_log_stats()
|
||||
|
||||
# Total count
|
||||
self.total_label.set_markup(f"<b>{stats['total']}</b> records")
|
||||
|
||||
# Level breakdown with colored indicators
|
||||
if stats["total"] == 0:
|
||||
status_text = "No log records"
|
||||
self.level_stats_label.set_text("No logs")
|
||||
else:
|
||||
level_counts = []
|
||||
parts = []
|
||||
level_colors = {
|
||||
"DEBUG": "#888888",
|
||||
"INFO": "#3584e4",
|
||||
"WARNING": "#e5a50a",
|
||||
"ERROR": "#e01b24",
|
||||
"CRITICAL": "#c01c28",
|
||||
}
|
||||
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
||||
count = stats["by_level"].get(level, 0)
|
||||
if count > 0:
|
||||
level_counts.append(f"{level}: {count}")
|
||||
color = level_colors.get(level, "#888888")
|
||||
parts.append(f"<span foreground='{color}'>{level[0]}</span>:{count}")
|
||||
|
||||
status_text = f"Total: {stats['total']} | " + " | ".join(level_counts)
|
||||
self.level_stats_label.set_markup(" ".join(parts))
|
||||
|
||||
self.status_label.set_text(status_text)
|
||||
# Buffer size indicator
|
||||
buffer_size = stats["total"]
|
||||
if buffer_size > 500:
|
||||
self.memory_label.set_markup(
|
||||
f"<small>Buffer: <span foreground='#e5a50a'>{buffer_size}/1000</span></small>"
|
||||
)
|
||||
else:
|
||||
self.memory_label.set_markup(f"<small>Buffer: {buffer_size}/1000</small>")
|
||||
|
||||
def _on_filter_changed(self, widget):
|
||||
"""Handle filter changes."""
|
||||
@@ -325,13 +624,7 @@ class LoggingDialog(Gtk.Dialog):
|
||||
|
||||
def _on_response(self, dialog, response_id):
|
||||
"""Handle dialog responses."""
|
||||
if response_id == Gtk.ResponseType.APPLY:
|
||||
self._export_logs()
|
||||
elif response_id == Gtk.ResponseType.REJECT:
|
||||
self._clear_logs()
|
||||
elif response_id == Gtk.ResponseType.HELP:
|
||||
self._copy_logs_to_clipboard()
|
||||
elif response_id == Gtk.ResponseType.CLOSE:
|
||||
if response_id == Gtk.ResponseType.CLOSE:
|
||||
self.destroy()
|
||||
|
||||
def _export_logs(self):
|
||||
@@ -341,9 +634,12 @@ class LoggingDialog(Gtk.Dialog):
|
||||
title="Export Logs", parent=self, action=Gtk.FileChooserAction.SAVE
|
||||
)
|
||||
|
||||
file_dialog.add_buttons(
|
||||
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK
|
||||
)
|
||||
file_dialog.add_buttons("_Cancel", Gtk.ResponseType.CANCEL, "_Save", Gtk.ResponseType.OK)
|
||||
|
||||
# Style the save button
|
||||
save_button = file_dialog.get_widget_for_response(Gtk.ResponseType.OK)
|
||||
if save_button:
|
||||
save_button.get_style_context().add_class("suggested-action")
|
||||
|
||||
# Set default filename
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
@@ -365,7 +661,7 @@ class LoggingDialog(Gtk.Dialog):
|
||||
)
|
||||
|
||||
if success:
|
||||
self._show_message("Export successful", f"Logs exported to:\n{filepath}")
|
||||
self._show_toast("Logs exported successfully")
|
||||
else:
|
||||
self._show_message(
|
||||
"Export failed",
|
||||
@@ -384,17 +680,13 @@ class LoggingDialog(Gtk.Dialog):
|
||||
text_content = self.text_buffer.get_text(start_iter, end_iter, False)
|
||||
|
||||
if not text_content.strip():
|
||||
self._show_message("No logs to copy", "There are no logs to copy to clipboard.")
|
||||
self._show_toast("No logs to copy")
|
||||
return
|
||||
|
||||
# Get the clipboard
|
||||
from gi.repository import Gdk
|
||||
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
|
||||
# Create header for clipboard content
|
||||
from datetime import datetime
|
||||
|
||||
header = f"VocaLinux Logs - Copied at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
header += "=" * 80 + "\n\n"
|
||||
|
||||
@@ -407,11 +699,7 @@ class LoggingDialog(Gtk.Dialog):
|
||||
|
||||
# Count lines for user feedback
|
||||
line_count = len(text_content.strip().split("\n"))
|
||||
self._show_message(
|
||||
"Logs copied to clipboard",
|
||||
f"Successfully copied {line_count} log lines to clipboard.\n\n"
|
||||
"You can now paste them into any text editor or document.",
|
||||
)
|
||||
self._show_toast(f"Copied {line_count} log lines to clipboard")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to copy logs to clipboard: {e}")
|
||||
@@ -424,9 +712,9 @@ class LoggingDialog(Gtk.Dialog):
|
||||
# Show confirmation dialog
|
||||
confirm_dialog = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.QUESTION,
|
||||
buttons=Gtk.ButtonsType.YES_NO,
|
||||
flags=Gtk.DialogFlags.MODAL,
|
||||
message_type=Gtk.MessageType.WARNING,
|
||||
buttons=Gtk.ButtonsType.NONE,
|
||||
text="Clear all logs?",
|
||||
)
|
||||
confirm_dialog.format_secondary_text(
|
||||
@@ -434,13 +722,60 @@ class LoggingDialog(Gtk.Dialog):
|
||||
"This action cannot be undone."
|
||||
)
|
||||
|
||||
confirm_dialog.add_button("_Cancel", Gtk.ResponseType.CANCEL)
|
||||
clear_btn = confirm_dialog.add_button("_Clear", Gtk.ResponseType.YES)
|
||||
clear_btn.get_style_context().add_class("destructive-action")
|
||||
|
||||
response = confirm_dialog.run()
|
||||
confirm_dialog.destroy()
|
||||
|
||||
if response == Gtk.ResponseType.YES:
|
||||
self.logging_manager.clear_logs()
|
||||
self._refresh_logs()
|
||||
self._show_message("Logs cleared", "All log records have been cleared.")
|
||||
self._show_toast("Logs cleared")
|
||||
|
||||
def _show_toast(self, message: str):
|
||||
"""Show a brief toast-style notification."""
|
||||
# For GTK3, we'll use an in-dialog notification
|
||||
# Create a revealer for smooth animation
|
||||
revealer = Gtk.Revealer()
|
||||
revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_DOWN)
|
||||
revealer.set_transition_duration(200)
|
||||
|
||||
toast_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||
toast_box.get_style_context().add_class("app-notification")
|
||||
toast_box.set_halign(Gtk.Align.CENTER)
|
||||
|
||||
# Style inline since CSS may not be loaded
|
||||
toast_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0.2, 0.2, 0.2, 0.9))
|
||||
|
||||
icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic", Gtk.IconSize.BUTTON)
|
||||
toast_box.pack_start(icon, False, False, 8)
|
||||
|
||||
label = Gtk.Label(label=message)
|
||||
label.set_margin_top(8)
|
||||
label.set_margin_bottom(8)
|
||||
label.set_margin_end(12)
|
||||
toast_box.pack_start(label, False, False, 0)
|
||||
|
||||
revealer.add(toast_box)
|
||||
|
||||
# Add to content area at top
|
||||
content_area = self.get_content_area()
|
||||
content_area.pack_start(revealer, False, False, 0)
|
||||
content_area.reorder_child(revealer, 0)
|
||||
revealer.show_all()
|
||||
|
||||
# Reveal with animation
|
||||
GLib.idle_add(revealer.set_reveal_child, True)
|
||||
|
||||
# Hide and remove after 2 seconds
|
||||
def hide_toast():
|
||||
revealer.set_reveal_child(False)
|
||||
GLib.timeout_add(200, lambda: (content_area.remove(revealer), False)[1])
|
||||
return False
|
||||
|
||||
GLib.timeout_add(2000, hide_toast)
|
||||
|
||||
def _show_message(
|
||||
self, title: str, message: str, message_type: Gtk.MessageType = Gtk.MessageType.INFO
|
||||
@@ -455,7 +790,7 @@ class LoggingDialog(Gtk.Dialog):
|
||||
"""
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
flags=0,
|
||||
flags=Gtk.DialogFlags.MODAL,
|
||||
message_type=message_type,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text=title,
|
||||
|
||||
+679
-649
File diff suppressed because it is too large
Load Diff
@@ -306,51 +306,10 @@ class TrayIndicator:
|
||||
|
||||
def _on_about_clicked(self, widget):
|
||||
"""Handle click on the About menu item."""
|
||||
from ..version import __copyright__, __description__, __url__, __version__
|
||||
from .about_dialog import show_about_dialog
|
||||
|
||||
logger.debug("About clicked")
|
||||
|
||||
about_dialog = Gtk.AboutDialog()
|
||||
about_dialog.set_program_name("Vocalinux")
|
||||
about_dialog.set_version(__version__)
|
||||
about_dialog.set_copyright(__copyright__)
|
||||
|
||||
# Authors and credits
|
||||
about_dialog.set_authors(["Jatin K Malik"])
|
||||
about_dialog.set_documenters(["Jatin K Malik"])
|
||||
about_dialog.set_artists(["Jatin K Malik"])
|
||||
|
||||
# Add credits section with social links
|
||||
about_dialog.add_credit_section(
|
||||
"Connect",
|
||||
[
|
||||
"GitHub: github.com/jatinkrmalik",
|
||||
"Twitter/X: @jatinkrmalik",
|
||||
],
|
||||
)
|
||||
|
||||
# Comments with better formatting
|
||||
about_dialog.set_comments(
|
||||
f"{__description__}\n\n" "🌟 Open Source Project\n" "Contributions Welcome!"
|
||||
)
|
||||
|
||||
about_dialog.set_website(__url__)
|
||||
about_dialog.set_website_label("⭐ Star on GitHub")
|
||||
about_dialog.set_license_type(Gtk.License.GPL_3_0)
|
||||
|
||||
# Set the logo using our custom icon
|
||||
logo_path = _resource_manager.get_icon_path("vocalinux")
|
||||
if os.path.exists(logo_path):
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(logo_path)
|
||||
scaled_pixbuf = pixbuf.scale_simple(128, 128, GdkPixbuf.InterpType.BILINEAR)
|
||||
about_dialog.set_logo(scaled_pixbuf)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load or scale logo: {e}")
|
||||
|
||||
# Run the dialog
|
||||
about_dialog.run()
|
||||
about_dialog.destroy()
|
||||
show_about_dialog(parent=None)
|
||||
|
||||
def _on_quit_clicked(self, widget):
|
||||
"""Handle click on the Quit menu item."""
|
||||
|
||||
@@ -251,12 +251,13 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
|
||||
def test_about_dialog(self):
|
||||
"""Test about dialog creation."""
|
||||
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
||||
with patch("vocalinux.ui.about_dialog.Gtk") as patched_gtk:
|
||||
mock_about_dialog = MagicMock()
|
||||
patched_gtk.AboutDialog.return_value = mock_about_dialog
|
||||
mock_about_dialog.run.side_effect = lambda: None
|
||||
patched_gtk.License.GPL_3_0 = 1
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.GdkPixbuf") as patched_pixbuf:
|
||||
with patch("vocalinux.ui.about_dialog.GdkPixbuf") as patched_pixbuf:
|
||||
mock_pixbuf = MagicMock()
|
||||
patched_pixbuf.Pixbuf.new_from_file.return_value = mock_pixbuf
|
||||
|
||||
@@ -429,13 +430,13 @@ class TestTrayIndicator(unittest.TestCase):
|
||||
|
||||
def test_about_dialog_logo_scaling_error(self):
|
||||
"""Test about dialog handles logo scaling errors gracefully."""
|
||||
with patch("vocalinux.ui.tray_indicator.Gtk") as patched_gtk:
|
||||
with patch("vocalinux.ui.about_dialog.Gtk") as patched_gtk:
|
||||
mock_about_dialog = MagicMock()
|
||||
patched_gtk.AboutDialog.return_value = mock_about_dialog
|
||||
mock_about_dialog.run.return_value = None
|
||||
patched_gtk.License.GPL_3_0 = 1
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.GdkPixbuf") as patched_pixbuf:
|
||||
with patch("vocalinux.ui.about_dialog.GdkPixbuf") as patched_pixbuf:
|
||||
# Simulate error loading pixbuf
|
||||
patched_pixbuf.Pixbuf.new_from_file.side_effect = Exception("Load error")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user