553 lines
No EOL
17 KiB
Perl
553 lines
No EOL
17 KiB
Perl
#!/usr/bin/perl
|
|
use strict;
|
|
use warnings;
|
|
use Gtk3 '-init';
|
|
use Glib qw(TRUE FALSE);
|
|
use File::Glob;
|
|
use FileHandle;
|
|
use LWP::Simple;
|
|
use Gtk3::SourceView;
|
|
|
|
# Declare global variables
|
|
our $textbuffer; # SourceView buffer for the editor
|
|
our $textview; # SourceView widget for editor
|
|
our $statusbar; # Status bar for feedback
|
|
our $open; # Current file path
|
|
our $track = 'init'; # Tracks modifications
|
|
|
|
# Base directory setup (reused from your code)
|
|
my ($ar) = @ARGV;
|
|
my $basedir;
|
|
if ($^O ne "Win32") {
|
|
if ($0 =~ /\//g) {
|
|
my @hd = split "/", $0;
|
|
pop @hd; # Remove filename
|
|
$basedir = join('/', @hd) || ".";
|
|
} else {
|
|
$basedir = ".";
|
|
}
|
|
} else {
|
|
$basedir = ".";
|
|
}
|
|
|
|
# Main window
|
|
my $window = Gtk3::Window->new('toplevel');
|
|
$window->set_title('KPad');
|
|
$window->set_default_size(800, 600);
|
|
$window->signal_connect(destroy => sub { Gtk3->main_quit });
|
|
|
|
# Main layout
|
|
my $vbox = Gtk3::Box->new('vertical', 0);
|
|
$window->add($vbox);
|
|
|
|
# Menu bar
|
|
my $menubar = Gtk3::MenuBar->new;
|
|
$vbox->pack_start($menubar, FALSE, FALSE, 0);
|
|
|
|
# File menu
|
|
my $file_menu = Gtk3::Menu->new;
|
|
my $file_item = Gtk3::MenuItem->new_with_label('File');
|
|
$file_item->set_submenu($file_menu);
|
|
$menubar->append($file_item);
|
|
|
|
my $new_item = Gtk3::MenuItem->new_with_label('New');
|
|
$new_item->signal_connect(activate => sub {
|
|
$textbuffer->set_text('');
|
|
$statusbar->push(0, 'New file');
|
|
});
|
|
$file_menu->append($new_item);
|
|
|
|
my $open_item = Gtk3::MenuItem->new_with_label('Open');
|
|
$open_item->signal_connect(activate => \&open_file);
|
|
$file_menu->append($open_item);
|
|
|
|
my $save_item = Gtk3::MenuItem->new_with_label('Save');
|
|
$save_item->signal_connect(activate => \&save_file);
|
|
$file_menu->append($save_item);
|
|
|
|
my $save_as_item = Gtk3::MenuItem->new_with_label('Save As');
|
|
$save_as_item->signal_connect(activate => \&save_as_file);
|
|
$file_menu->append($save_as_item);
|
|
|
|
my $exit_item = Gtk3::MenuItem->new_with_label('Exit');
|
|
$exit_item->signal_connect(activate => \&tapp);
|
|
$file_menu->append($exit_item);
|
|
|
|
# Edit menu
|
|
my $edit_menu = Gtk3::Menu->new;
|
|
my $edit_item = Gtk3::MenuItem->new_with_label('Edit');
|
|
$edit_item->set_submenu($edit_menu);
|
|
$menubar->append($edit_item);
|
|
|
|
my $undo_item = Gtk3::MenuItem->new_with_label('Undo');
|
|
$undo_item->signal_connect(activate => sub {
|
|
$textbuffer->undo if $textbuffer->can_undo;
|
|
$statusbar->push(0, 'Undo');
|
|
});
|
|
$edit_menu->append($undo_item);
|
|
|
|
my $redo_item = Gtk3::MenuItem->new_with_label('Redo');
|
|
$redo_item->signal_connect(activate => sub {
|
|
$textbuffer->redo if $textbuffer->can_redo;
|
|
$statusbar->push(0, 'Redo');
|
|
});
|
|
$edit_menu->append($redo_item);
|
|
|
|
my $cut_item = Gtk3::MenuItem->new_with_label('Cut');
|
|
$cut_item->signal_connect(activate => sub {
|
|
$textbuffer->cut_clipboard(Gtk3::Clipboard::get('CLIPBOARD'), TRUE);
|
|
});
|
|
$edit_menu->append($cut_item);
|
|
|
|
my $copy_item = Gtk3::MenuItem->new_with_label('Copy');
|
|
$copy_item->signal_connect(activate => sub {
|
|
$textbuffer->copy_clipboard(Gtk3::Clipboard::get('CLIPBOARD'));
|
|
});
|
|
$edit_menu->append($copy_item);
|
|
|
|
my $paste_item = Gtk3::MenuItem->new_with_label('Paste');
|
|
$paste_item->signal_connect(activate => sub {
|
|
$textbuffer->paste_clipboard(Gtk3::Clipboard::get('CLIPBOARD'), undef, TRUE);
|
|
});
|
|
$edit_menu->append($paste_item);
|
|
|
|
my $select_all_item = Gtk3::MenuItem->new_with_label('Select All');
|
|
$select_all_item->signal_connect(activate => sub {
|
|
$textbuffer->select_range($textbuffer->get_start_iter, $textbuffer->get_end_iter);
|
|
});
|
|
$edit_menu->append($select_all_item);
|
|
|
|
my $find_item = Gtk3::MenuItem->new_with_label('Find');
|
|
$find_item->signal_connect(activate => sub { find_and_replace_dialog(1); });
|
|
$edit_menu->append($find_item);
|
|
|
|
my $replace_item = Gtk3::MenuItem->new_with_label('Find and Replace');
|
|
$replace_item->signal_connect(activate => sub { find_and_replace_dialog(0); });
|
|
$edit_menu->append($replace_item);
|
|
|
|
# View menu (wrap options)
|
|
my $view_menu = Gtk3::Menu->new;
|
|
my $view_item = Gtk3::MenuItem->new_with_label('View');
|
|
$view_item->set_submenu($view_menu);
|
|
$menubar->append($view_item);
|
|
|
|
my $wrap_menu = Gtk3::Menu->new;
|
|
my $wrap_item = Gtk3::MenuItem->new_with_label('Wrap');
|
|
$wrap_item->set_submenu($wrap_menu);
|
|
$view_menu->append($wrap_item);
|
|
|
|
my $wrap_word = Gtk3::CheckMenuItem->new_with_label('Word');
|
|
$wrap_word->signal_connect(toggled => sub {
|
|
$textview->set_wrap_mode($wrap_word->get_active ? 'word' : 'none');
|
|
});
|
|
$wrap_menu->append($wrap_word);
|
|
|
|
my $wrap_char = Gtk3::CheckMenuItem->new_with_label('Char');
|
|
$wrap_char->signal_connect(toggled => sub {
|
|
$textview->set_wrap_mode($wrap_char->get_active ? 'char' : 'none');
|
|
});
|
|
$wrap_menu->append($wrap_char);
|
|
|
|
my $wrap_none = Gtk3::CheckMenuItem->new_with_label('None');
|
|
$wrap_none->signal_connect(toggled => sub {
|
|
$textview->set_wrap_mode($wrap_none->get_active ? 'none' : 'none');
|
|
});
|
|
$wrap_menu->append($wrap_none);
|
|
|
|
# Tools menu
|
|
my $tools_menu = Gtk3::Menu->new;
|
|
my $tools_item = Gtk3::MenuItem->new_with_label('Tools');
|
|
$tools_item->set_submenu($tools_menu);
|
|
$menubar->append($tools_item);
|
|
|
|
my $goto_line_item = Gtk3::MenuItem->new_with_label('Goto Line');
|
|
$goto_line_item->signal_connect(activate => \&goto_line_dialog);
|
|
$tools_menu->append($goto_line_item);
|
|
|
|
my $which_line_item = Gtk3::MenuItem->new_with_label('Which Line?');
|
|
$which_line_item->signal_connect(activate => \&which_line_dialog);
|
|
$tools_menu->append($which_line_item);
|
|
|
|
my $run_pshell = Gtk3::MenuItem->new_with_label('Run pshell');
|
|
$run_pshell->signal_connect(activate => sub {
|
|
my $pshell_path = "$basedir/pshell";
|
|
my $terminal = find_terminal();
|
|
if (-x $pshell_path) {
|
|
system("$terminal -e $pshell_path &");
|
|
$statusbar->push(0, "Launched pshell in $terminal");
|
|
} else {
|
|
$statusbar->push(0, "Error: pshell not found at $pshell_path");
|
|
}
|
|
});
|
|
$tools_menu->append($run_pshell);
|
|
|
|
# HTML menu
|
|
my $html_menu = Gtk3::Menu->new;
|
|
my $html_item = Gtk3::MenuItem->new_with_label('HTML');
|
|
$html_item->set_submenu($html_menu);
|
|
$menubar->append($html_item);
|
|
|
|
my $fetch_item = Gtk3::MenuItem->new_with_label('Fetch a web resource...');
|
|
$fetch_item->signal_connect(activate => \&fetch_html);
|
|
$html_menu->append($fetch_item);
|
|
|
|
# Macros menu (plugins)
|
|
my $macros_menu = Gtk3::Menu->new;
|
|
my $macros_item = Gtk3::MenuItem->new_with_label('Macros');
|
|
$macros_item->set_submenu($macros_menu);
|
|
$menubar->append($macros_item);
|
|
|
|
my $exec_macro_item = Gtk3::MenuItem->new_with_label('Execute Macro');
|
|
$exec_macro_item->signal_connect(activate => \&show_plugin_dialog);
|
|
$macros_menu->append($exec_macro_item);
|
|
|
|
# Help menu
|
|
my $help_menu = Gtk3::Menu->new;
|
|
my $help_item = Gtk3::MenuItem->new_with_label('Help');
|
|
$help_item->set_submenu($help_menu);
|
|
$menubar->append($help_item);
|
|
|
|
my $help_topics_item = Gtk3::MenuItem->new_with_label('Help Topics...');
|
|
$help_topics_item->signal_connect(activate => sub {
|
|
my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok',
|
|
"Help topics for KPAD\nWell, this is a text/file editor mainly meant for scripting and programming use.\nLike notepad but made for the programmer.");
|
|
$dialog->run;
|
|
$dialog->destroy;
|
|
});
|
|
$help_menu->append($help_topics_item);
|
|
|
|
my $about_item = Gtk3::MenuItem->new_with_label('About KPAD...');
|
|
$about_item->signal_connect(activate => sub {
|
|
my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok',
|
|
"kPad\nby Paul Malcher\nVersion 6 Release\n");
|
|
$dialog->run;
|
|
$dialog->destroy;
|
|
});
|
|
$help_menu->append($about_item);
|
|
|
|
# Editor (SourceView)
|
|
my $scrolled = Gtk3::ScrolledWindow->new(undef, undef);
|
|
$vbox->pack_start($scrolled, TRUE, TRUE, 0);
|
|
$textview = Gtk3::SourceView::View->new;
|
|
$textview->set_wrap_mode('word');
|
|
$textview->set_show_line_numbers(TRUE);
|
|
$textview->set_auto_indent(TRUE);
|
|
$scrolled->add($textview);
|
|
$textbuffer = $textview->get_buffer;
|
|
my $lang_manager = Gtk3::SourceView::LanguageManager->get_default;
|
|
my $language = $lang_manager->get_language('perl');
|
|
$textbuffer->set_language($language) if $language;
|
|
$textbuffer->set_highlight_syntax(TRUE);
|
|
|
|
# CSS styling for Kubuntu
|
|
my $provider = Gtk3::CssProvider->new;
|
|
$provider->load_from_data('
|
|
textview { font-family: Monospace; font-size: 12pt; }
|
|
button { color: black; background: #d3d3d3; }
|
|
');
|
|
Gtk3::StyleContext::add_provider_for_screen(
|
|
Gtk3::Gdk::Screen::get_default,
|
|
$provider,
|
|
Gtk3::STYLE_PROVIDER_PRIORITY_APPLICATION
|
|
);
|
|
|
|
# Status bar
|
|
$statusbar = Gtk3::Statusbar->new;
|
|
$vbox->pack_start($statusbar, FALSE, FALSE, 0);
|
|
$textbuffer->signal_connect('mark-set' => sub {
|
|
my ($buf, $iter, $mark) = @_;
|
|
if ($mark->get_name eq 'insert') {
|
|
my $line = $iter->get_line + 1;
|
|
$statusbar->push(0, "Line: $line");
|
|
}
|
|
});
|
|
|
|
# Track modifications
|
|
$textbuffer->signal_connect('modified-changed' => sub {
|
|
$track = $textbuffer->get_text($textbuffer->get_start_iter, $textbuffer->get_end_iter, TRUE);
|
|
});
|
|
|
|
# Load file from ARGV
|
|
if ($ar) {
|
|
if (open my $fh, '<', $ar) {
|
|
local $/;
|
|
$textbuffer->set_text(<$fh>);
|
|
close $fh;
|
|
$statusbar->push(0, "Opened $ar");
|
|
$open = $ar;
|
|
} else {
|
|
$statusbar->push(0, "Failed to open $ar");
|
|
}
|
|
}
|
|
|
|
# Plugin system
|
|
my @plugins;
|
|
my @n;
|
|
my $pls = 0;
|
|
while (<*.kpd>) {
|
|
push @plugins, $_;
|
|
open my $pin, '<', $_ or next;
|
|
my @gn = split '::', <$pin>;
|
|
$n[$pls] = $gn[2] eq 'auto' ? 'auto' : $gn[1];
|
|
$pls++;
|
|
}
|
|
my $nop = grep { $_ ne 'auto' } @n;
|
|
|
|
# Find available terminal
|
|
sub find_terminal {
|
|
my @terminals = qw(gnome-terminal konsole xterm);
|
|
for my $term (@terminals) {
|
|
return $term if `which $term 2>/dev/null` =~ /\S/;
|
|
}
|
|
return 'xterm'; # Fallback
|
|
}
|
|
|
|
# File handling
|
|
sub open_file {
|
|
my $dialog = Gtk3::FileChooserDialog->new(
|
|
'Open File', $window, 'open',
|
|
'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok'
|
|
);
|
|
my $pl_filter = Gtk3::FileFilter->new;
|
|
$pl_filter->set_name('Perl Scripts');
|
|
$pl_filter->add_pattern('*.pl');
|
|
$dialog->add_filter($pl_filter);
|
|
my $all_filter = Gtk3::FileFilter->new;
|
|
$all_filter->set_name('All Files');
|
|
$all_filter->add_pattern('*');
|
|
$dialog->add_filter($all_filter);
|
|
my $response = $dialog->run;
|
|
if ($response && $response eq 'ok') {
|
|
my $filename = $dialog->get_filename;
|
|
if ($filename && open my $fh, '<', $filename) {
|
|
local $/;
|
|
$textbuffer->set_text(<$fh>);
|
|
close $fh;
|
|
$statusbar->push(0, "Opened $filename");
|
|
$open = $filename;
|
|
} else {
|
|
$statusbar->push(0, "Failed to open $filename");
|
|
}
|
|
}
|
|
$dialog->destroy;
|
|
}
|
|
|
|
sub save_file {
|
|
if ($open) {
|
|
my ($start, $end) = $textbuffer->get_bounds;
|
|
my $text = $textbuffer->get_text($start, $end, TRUE);
|
|
if (open my $fh, '>', $open) {
|
|
print $fh $text;
|
|
close $fh;
|
|
$statusbar->push(0, "Saved $open");
|
|
} else {
|
|
$statusbar->push(0, "Failed to save $open");
|
|
}
|
|
} else {
|
|
save_as_file();
|
|
}
|
|
}
|
|
|
|
sub save_as_file {
|
|
my $dialog = Gtk3::FileChooserDialog->new(
|
|
'Save File', $window, 'save',
|
|
'gtk-cancel' => 'cancel', 'gtk-ok' => 'ok'
|
|
);
|
|
my $pl_filter = Gtk3::FileFilter->new;
|
|
$pl_filter->set_name('Perl Scripts');
|
|
$pl_filter->add_pattern('*.pl');
|
|
$dialog->add_filter($pl_filter);
|
|
my $all_filter = Gtk3::FileFilter->new;
|
|
$all_filter->set_name('All Files');
|
|
$all_filter->add_pattern('*');
|
|
$dialog->add_filter($all_filter);
|
|
my $response = $dialog->run;
|
|
if ($response && $response eq 'ok') {
|
|
my $filename = $dialog->get_filename;
|
|
if ($filename) {
|
|
my ($start, $end) = $textbuffer->get_bounds;
|
|
my $text = $textbuffer->get_text($start, $end, TRUE);
|
|
if (open my $fh, '>', $filename) {
|
|
print $fh $text;
|
|
close $fh;
|
|
$statusbar->push(0, "Saved $filename");
|
|
$open = $filename;
|
|
} else {
|
|
$statusbar->push(0, "Failed to save $filename");
|
|
}
|
|
}
|
|
}
|
|
$dialog->destroy;
|
|
}
|
|
|
|
# Find and replace dialog
|
|
sub find_and_replace_dialog {
|
|
my ($find_only) = @_;
|
|
my $dialog = Gtk3::Dialog->new($find_only ? 'Find' : 'Find and Replace', $window, 'modal',
|
|
'gtk-ok', 'ok', 'gtk-cancel', 'cancel');
|
|
my $find_entry = Gtk3::Entry->new;
|
|
my $replace_entry = Gtk3::Entry->new;
|
|
my $find_label = Gtk3::Label->new('Find:');
|
|
$dialog->get_content_area->pack_start($find_label, FALSE, FALSE, 0);
|
|
$dialog->get_content_area->pack_start($find_entry, FALSE, FALSE, 0);
|
|
unless ($find_only) {
|
|
my $replace_label = Gtk3::Label->new('Replace with:');
|
|
$dialog->get_content_area->pack_start($replace_label, FALSE, FALSE, 0);
|
|
$dialog->get_content_area->pack_start($replace_entry, FALSE, FALSE, 0);
|
|
}
|
|
$dialog->show_all;
|
|
if ($dialog->run eq 'ok') {
|
|
my $search = $find_entry->get_text;
|
|
my $replace = $replace_entry->get_text;
|
|
my $search_context = Gtk3::SourceView::SearchContext->new($textbuffer);
|
|
$search_context->set_search_text($search, []);
|
|
my $iter = $textbuffer->get_start_iter;
|
|
if (my ($match_start, $match_end) = $search_context->forward($iter)) {
|
|
$textbuffer->select_range($match_start, $match_end);
|
|
unless ($find_only) {
|
|
$search_context->replace($match_start, $match_end, $replace, -1);
|
|
$statusbar->push(0, "Replaced '$search' with '$replace'");
|
|
} else {
|
|
$statusbar->push(0, "Found '$search'");
|
|
}
|
|
} else {
|
|
$statusbar->push(0, "Text not found");
|
|
}
|
|
}
|
|
$dialog->destroy;
|
|
}
|
|
|
|
# Goto line dialog
|
|
sub goto_line_dialog {
|
|
my $dialog = Gtk3::Dialog->new('Goto Line', $window, 'modal', 'gtk-ok', 'ok', 'gtk-cancel', 'cancel');
|
|
my $entry = Gtk3::Entry->new;
|
|
$dialog->get_content_area->pack_start($entry, FALSE, FALSE, 0);
|
|
$dialog->show_all;
|
|
if ($dialog->run eq 'ok') {
|
|
my $line = $entry->get_text;
|
|
if ($line =~ /^\d+$/) {
|
|
my $iter = $textbuffer->get_iter_at_line($line - 1);
|
|
$textbuffer->place_cursor($iter);
|
|
$textview->scroll_to_iter($iter, 0, TRUE, 0, 0.5);
|
|
}
|
|
}
|
|
$dialog->destroy;
|
|
}
|
|
|
|
# Which line dialog
|
|
sub which_line_dialog {
|
|
my $iter = $textbuffer->get_iter_at_mark($textbuffer->get_insert);
|
|
my $line = $iter->get_line + 1;
|
|
my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'info', 'ok', "Current line: $line");
|
|
$dialog->run;
|
|
$dialog->destroy;
|
|
}
|
|
|
|
# HTML fetch
|
|
sub fetch_html {
|
|
my $dialog = Gtk3::Dialog->new('HTML Source Fetch', $window, 'modal', 'gtk-ok', 'ok', 'gtk-cancel', 'cancel');
|
|
my $label = Gtk3::Label->new('Fetch what:');
|
|
my $entry = Gtk3::Entry->new;
|
|
$entry->set_text('http://');
|
|
$dialog->get_content_area->pack_start($label, FALSE, FALSE, 0);
|
|
$dialog->get_content_area->pack_start($entry, FALSE, FALSE, 0);
|
|
$dialog->show_all;
|
|
if ($dialog->run eq 'ok') {
|
|
my $url = $entry->get_text;
|
|
my $contents = get($url);
|
|
if ($contents) {
|
|
$textbuffer->set_text($contents);
|
|
$statusbar->push(0, "Fetched $url");
|
|
} else {
|
|
$statusbar->push(0, "Failed to fetch $url");
|
|
}
|
|
}
|
|
$dialog->destroy;
|
|
}
|
|
|
|
# Plugin dialog
|
|
sub show_plugin_dialog {
|
|
my $dialog = Gtk3::Dialog->new('Macro Execution Menu', $window, 'modal', 'gtk-close', 'close');
|
|
my $label = Gtk3::Label->new('Double Click To Execute Macro');
|
|
my $listbox = Gtk3::ListBox->new;
|
|
$dialog->get_content_area->pack_start($label, FALSE, FALSE, 0);
|
|
$dialog->get_content_area->pack_start($listbox, TRUE, TRUE, 0);
|
|
foreach my $name (@n) {
|
|
next if $name eq 'auto';
|
|
my $row = Gtk3::ListBoxRow->new;
|
|
my $lbl = Gtk3::Label->new($name);
|
|
$row->add($lbl);
|
|
$listbox->add($row);
|
|
}
|
|
$listbox->signal_connect(row_activated => sub {
|
|
my ($lb, $row) = @_;
|
|
my $name = $row->get_child->get_text;
|
|
eplugin($name);
|
|
});
|
|
$dialog->show_all;
|
|
$dialog->run;
|
|
$dialog->destroy;
|
|
}
|
|
|
|
# Plugin execution
|
|
sub eplugin {
|
|
my ($v) = @_;
|
|
my $fp = 0;
|
|
while ($n[$fp] ne $v) { $fp++; }
|
|
$v = $plugins[$fp];
|
|
open my $pe, '<', "$basedir/$v" or return;
|
|
my $tdata = do { local $/; <$pe> };
|
|
close $pe;
|
|
$tdata =~ s/.*?\n//; # Skip first line
|
|
eval $tdata;
|
|
if ($@) {
|
|
my $error = $@;
|
|
my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'error', 'ok', "Error: $error");
|
|
$dialog->run;
|
|
$dialog->destroy;
|
|
}
|
|
}
|
|
|
|
# Auto plugin execution
|
|
sub aeplugin {
|
|
for my $i (0..$#n) {
|
|
next unless $n[$i] eq 'auto';
|
|
my $v = $plugins[$i];
|
|
open my $pe, '<', "$basedir/$v" or next;
|
|
my $tdata = do { local $/; <$pe> };
|
|
close $pe;
|
|
$tdata =~ s/.*?\n//;
|
|
eval $tdata;
|
|
if ($@) {
|
|
my $error = $@;
|
|
my $dialog = Gtk3::MessageDialog->new($window, 'modal', 'error', 'ok', "Error: $error");
|
|
$dialog->run;
|
|
$dialog->destroy;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Shutdown handler
|
|
sub tapp {
|
|
if ($track ne 'init') {
|
|
my $dialog = Gtk3::MessageDialog->new(
|
|
$window, 'modal', 'question', 'yes-no', 'File contents have changed, save now?!'
|
|
);
|
|
my $response = $dialog->run;
|
|
$dialog->destroy;
|
|
if ($response eq 'yes') {
|
|
save_file();
|
|
}
|
|
}
|
|
Gtk3->main_quit;
|
|
}
|
|
|
|
# Run auto plugins
|
|
my $arun = 0;
|
|
if ($arun eq '0') {
|
|
aeplugin();
|
|
}
|
|
|
|
# Show and start
|
|
$window->show_all;
|
|
Gtk3->main; |