698 lines
20 KiB
Perl
Executable file
698 lines
20 KiB
Perl
Executable file
#!/usr/bin/perl
|
|
|
|
# KAKE PAD version 6.0
|
|
# GTK3 version converted from TK
|
|
|
|
# After hours having a AI go around in circles for the error
|
|
|
|
#*** unhandled exception in callback:
|
|
#*** FATAL: invalid GdkModifierType value 4, expecting a string scalar or an arrayref of strings at kpad.pl line 114.
|
|
#*** ignoring at /usr/share/perl5/Gtk3.pm line 572.
|
|
#Use of uninitialized value in string eq at kpad.pl line 102.
|
|
|
|
#use strict;
|
|
#use warnings;
|
|
# There I fixed it, by commenting the two lines above out
|
|
|
|
use Gtk3 -init;
|
|
use File::Glob;
|
|
use File::Find;
|
|
use FileHandle;
|
|
use LWP::Simple;
|
|
use Encode qw(decode encode);
|
|
|
|
# Global variables for file handling
|
|
my $current_file;
|
|
my $text_buffer;
|
|
my @undo_stack = ();
|
|
my @redo_stack = ();
|
|
my $ignore_changes = 0;
|
|
my $status_bar;
|
|
my $last_search_pos;
|
|
my $search_dialog;
|
|
my $replace_dialog;
|
|
|
|
use constant CONTROL_MASK => 'control-mask'; # Update CONTROL_MASK constant to use the correct string value
|
|
|
|
# Initialize the main window
|
|
my $window = Gtk3::Window->new('toplevel');
|
|
$window->set_title("kPad");
|
|
$window->set_default_size(800, 600);
|
|
|
|
# Create the main vertical box
|
|
my $vbox = Gtk3::Box->new('vertical', 5);
|
|
$window->add($vbox);
|
|
|
|
# Create menubar
|
|
my $menubar = Gtk3::MenuBar->new();
|
|
$vbox->pack_start($menubar, 0, 0, 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);
|
|
|
|
# File menu items
|
|
my $new_item = Gtk3::MenuItem->new_with_label('New');
|
|
my $open_item = Gtk3::MenuItem->new_with_label('Open');
|
|
my $save_item = Gtk3::MenuItem->new_with_label('Save');
|
|
my $save_as_item = Gtk3::MenuItem->new_with_label('Save As');
|
|
my $separator = Gtk3::SeparatorMenuItem->new();
|
|
my $exit_item = Gtk3::MenuItem->new_with_label('Exit');
|
|
|
|
$file_menu->append($_) for ($new_item, $open_item, $save_item, $save_as_item, $separator, $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);
|
|
|
|
# Edit menu items
|
|
my $undo_item = Gtk3::MenuItem->new_with_label('Undo');
|
|
my $redo_item = Gtk3::MenuItem->new_with_label('Redo');
|
|
my $cut_item = Gtk3::MenuItem->new_with_label('Cut');
|
|
my $copy_item = Gtk3::MenuItem->new_with_label('Copy');
|
|
my $paste_item = Gtk3::MenuItem->new_with_label('Paste');
|
|
my $find_item = Gtk3::MenuItem->new_with_label('Find');
|
|
my $replace_item = Gtk3::MenuItem->new_with_label('Find and Replace');
|
|
my $select_all_item = Gtk3::MenuItem->new_with_label('Select All');
|
|
|
|
$edit_menu->append($_) for (
|
|
$undo_item, $redo_item,
|
|
Gtk3::SeparatorMenuItem->new(),
|
|
$cut_item, $copy_item, $paste_item,
|
|
Gtk3::SeparatorMenuItem->new(),
|
|
$find_item, $replace_item,
|
|
Gtk3::SeparatorMenuItem->new(),
|
|
$select_all_item
|
|
);
|
|
|
|
# Add Tools menu
|
|
my $tools_menu = Gtk3::Menu->new();
|
|
my $tools_item = Gtk3::MenuItem->new_with_label('Tools');
|
|
$tools_item->set_submenu($tools_menu);
|
|
|
|
# Add About menu
|
|
my $about_item = Gtk3::MenuItem->new_with_label('About');
|
|
$about_item->signal_connect(activate => sub {
|
|
show_info_dialog('About kPad', 'kPad version 6.0\nGTK3 version');
|
|
});
|
|
|
|
# Add HTML menu
|
|
my $html_item = Gtk3::MenuItem->new_with_label('HTML');
|
|
$html_item->signal_connect(activate => sub {
|
|
# Placeholder for HTML functionality
|
|
show_info_dialog('HTML', 'HTML functionality will be added soon.');
|
|
});
|
|
|
|
# Add Macros menu (placeholder)
|
|
my $macros_item = Gtk3::MenuItem->new_with_label('Macros');
|
|
$macros_item->signal_connect(activate => sub {
|
|
# Placeholder for Macros functionality
|
|
show_info_dialog('Macros', 'Macros functionality will be handled by the user.');
|
|
});
|
|
|
|
# Add new menu items to the menu bar
|
|
$menubar->append($tools_item);
|
|
$menubar->append($about_item);
|
|
$menubar->append($html_item);
|
|
$menubar->append($macros_item);
|
|
|
|
# Create text view with scrolled window
|
|
my $scrolled_window = Gtk3::ScrolledWindow->new();
|
|
$scrolled_window->set_policy('automatic', 'automatic');
|
|
$vbox->pack_start($scrolled_window, 1, 1, 0);
|
|
|
|
my $text_view = Gtk3::TextView->new();
|
|
$text_buffer = $text_view->get_buffer();
|
|
$text_view->set_wrap_mode('word');
|
|
$text_view->set_editable(1);
|
|
$text_view->set_cursor_visible(1);
|
|
|
|
# Create status bar
|
|
$status_bar = Gtk3::Statusbar->new();
|
|
$vbox->pack_start($status_bar, 0, 0, 0);
|
|
update_cursor_position();
|
|
|
|
# Track cursor movement for status bar updates
|
|
$text_buffer->signal_connect('mark-set' => sub {
|
|
my ($buffer, $iter, $mark) = @_;
|
|
if ($mark->get_name() eq 'insert') {
|
|
update_cursor_position();
|
|
}
|
|
});
|
|
|
|
# Enable copy/paste keyboard shortcuts
|
|
$text_view->signal_connect('key-press-event' => sub {
|
|
my ($widget, $event) = @_;
|
|
my $keyval = $event->keyval;
|
|
my $state = $event->state;
|
|
|
|
# Check if Control key is pressed (GDK_CONTROL_MASK)
|
|
if ($state & CONTROL_MASK) {
|
|
if (chr($keyval) eq 'z' || chr($keyval) eq 'Z') {
|
|
undo_action();
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'y' || chr($keyval) eq 'Y') {
|
|
redo_action();
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'c' || chr($keyval) eq 'C') {
|
|
$copy_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'x' || chr($keyval) eq 'X') {
|
|
$cut_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'v' || chr($keyval) eq 'V') {
|
|
$paste_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'f' || chr($keyval) eq 'F') {
|
|
$find_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'h' || chr($keyval) eq 'H') {
|
|
$replace_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
# Track buffer changes for undo/redo
|
|
$text_buffer->signal_connect('changed' => \&buffer_changed_cb);
|
|
$text_buffer->signal_connect('begin-user-action' => sub {
|
|
$ignore_changes = 0;
|
|
});
|
|
$text_buffer->signal_connect('modified-changed' => sub {
|
|
my $modified = $text_buffer->get_modified();
|
|
$window->set_title("kPad" . ($modified ? " *" : "") . (defined $current_file ? " - $current_file" : ""));
|
|
});
|
|
|
|
$scrolled_window->add($text_view);
|
|
|
|
# File menu callbacks
|
|
$new_item->signal_connect(activate => sub {
|
|
return unless check_save_changes();
|
|
|
|
$text_buffer->set_text('');
|
|
$text_buffer->set_modified(0);
|
|
$current_file = undef;
|
|
$window->set_title("kPad");
|
|
});
|
|
|
|
$open_item->signal_connect(activate => sub {
|
|
return unless check_save_changes();
|
|
|
|
my $dialog = Gtk3::FileChooserDialog->new(
|
|
'Open File',
|
|
$window,
|
|
'open',
|
|
'Cancel' => 'cancel',
|
|
'Open' => 'ok'
|
|
);
|
|
|
|
# Add file filters
|
|
my $filter = Gtk3::FileFilter->new();
|
|
$filter->set_name('Perl Scripts');
|
|
$filter->add_pattern('*.pl');
|
|
$dialog->add_filter($filter);
|
|
|
|
$filter = Gtk3::FileFilter->new();
|
|
$filter->set_name('All Files');
|
|
$filter->add_pattern('*');
|
|
$dialog->add_filter($filter);
|
|
|
|
if ('ok' eq $dialog->run()) {
|
|
my $filename = $dialog->get_filename();
|
|
open_file($filename);
|
|
}
|
|
$dialog->destroy();
|
|
});
|
|
|
|
$save_item->signal_connect(activate => sub {
|
|
if (defined $current_file) {
|
|
save_file($current_file);
|
|
} else {
|
|
$save_as_item->signal_emit('activate');
|
|
}
|
|
});
|
|
|
|
$save_as_item->signal_connect(activate => sub {
|
|
my $dialog = Gtk3::FileChooserDialog->new(
|
|
'Save File',
|
|
$window,
|
|
'save',
|
|
'Cancel' => 'cancel',
|
|
'Save' => 'ok'
|
|
);
|
|
|
|
# Add file filters
|
|
my $filter = Gtk3::FileFilter->new();
|
|
$filter->set_name('Perl Scripts');
|
|
$filter->add_pattern('*.pl');
|
|
$dialog->add_filter($filter);
|
|
|
|
$filter = Gtk3::FileFilter->new();
|
|
$filter->set_name('All Files');
|
|
$filter->add_pattern('*');
|
|
$dialog->add_filter($filter);
|
|
|
|
if ('ok' eq $dialog->run()) {
|
|
my $filename = $dialog->get_filename();
|
|
save_file($filename);
|
|
}
|
|
$dialog->destroy();
|
|
});
|
|
|
|
# Edit menu callbacks
|
|
$undo_item->signal_connect(activate => sub {
|
|
undo_action();
|
|
});
|
|
|
|
$redo_item->signal_connect(activate => sub {
|
|
redo_action();
|
|
});
|
|
|
|
$cut_item->signal_connect(activate => sub {
|
|
$text_buffer->cut_clipboard(
|
|
Gtk3::Clipboard::get(Gtk3::Gdk::Atom::intern('CLIPBOARD', 0)),
|
|
1
|
|
);
|
|
});
|
|
|
|
$copy_item->signal_connect(activate => sub {
|
|
$text_buffer->copy_clipboard(
|
|
Gtk3::Clipboard::get(Gtk3::Gdk::Atom::intern('CLIPBOARD', 0))
|
|
);
|
|
});
|
|
|
|
$paste_item->signal_connect(activate => sub {
|
|
$text_buffer->paste_clipboard(
|
|
Gtk3::Clipboard::get(Gtk3::Gdk::Atom::intern('CLIPBOARD', 0)),
|
|
undef,
|
|
1
|
|
);
|
|
});
|
|
|
|
$select_all_item->signal_connect(activate => sub {
|
|
my $start = $text_buffer->get_start_iter();
|
|
my $end = $text_buffer->get_end_iter();
|
|
$text_buffer->select_range($start, $end);
|
|
});
|
|
|
|
# Connect find/replace menu items
|
|
$find_item->signal_connect(activate => sub {
|
|
create_search_dialog();
|
|
$search_dialog->present();
|
|
});
|
|
|
|
$replace_item->signal_connect(activate => sub {
|
|
create_replace_dialog();
|
|
$replace_dialog->present();
|
|
});
|
|
|
|
# Helper functions
|
|
sub open_file {
|
|
my ($filename) = @_;
|
|
return unless -f $filename;
|
|
|
|
if (open(my $fh, '<:encoding(UTF-8)', $filename)) {
|
|
local $/;
|
|
my $content = <$fh>;
|
|
close $fh;
|
|
|
|
$text_buffer->set_text($content);
|
|
$text_buffer->set_modified(0);
|
|
$current_file = $filename;
|
|
$window->set_title("kPad - $filename");
|
|
} else {
|
|
show_error_dialog("Could not open file: $!");
|
|
}
|
|
}
|
|
|
|
sub save_file {
|
|
my ($filename) = @_;
|
|
return unless defined $filename;
|
|
|
|
if (open(my $fh, '>:encoding(UTF-8)', $filename)) {
|
|
my $content = $text_buffer->get_text(
|
|
$text_buffer->get_start_iter(),
|
|
$text_buffer->get_end_iter(),
|
|
1
|
|
);
|
|
print $fh $content;
|
|
close $fh;
|
|
|
|
$current_file = $filename;
|
|
$text_buffer->set_modified(0);
|
|
$window->set_title("kPad - $filename");
|
|
} else {
|
|
show_error_dialog("Could not save file: $!");
|
|
}
|
|
}
|
|
|
|
sub buffer_changed {
|
|
return $text_buffer->get_modified();
|
|
}
|
|
|
|
sub check_save_changes {
|
|
if (buffer_changed()) {
|
|
my $dialog = Gtk3::MessageDialog->new(
|
|
$window,
|
|
'modal',
|
|
'question',
|
|
'none', # No default buttons
|
|
"The document has been modified. Save changes?"
|
|
);
|
|
|
|
$dialog->add_button('Save', 'yes');
|
|
$dialog->add_button("Don't Save", 'no');
|
|
$dialog->add_button('Cancel', 'cancel');
|
|
|
|
my $response = $dialog->run();
|
|
$dialog->destroy();
|
|
|
|
if ($response eq 'yes') {
|
|
if (defined $current_file) {
|
|
save_file($current_file);
|
|
return 1;
|
|
} else {
|
|
my $save_dialog = Gtk3::FileChooserDialog->new(
|
|
'Save File',
|
|
$window,
|
|
'save',
|
|
'Cancel' => 'cancel',
|
|
'Save' => 'ok'
|
|
);
|
|
|
|
# Add file filters
|
|
my $filter = Gtk3::FileFilter->new();
|
|
$filter->set_name('Perl Scripts');
|
|
$filter->add_pattern('*.pl');
|
|
$save_dialog->add_filter($filter);
|
|
|
|
$filter = Gtk3::FileFilter->new();
|
|
$filter->set_name('All Files');
|
|
$filter->add_pattern('*');
|
|
$save_dialog->add_filter($filter);
|
|
|
|
my $save_response = $save_dialog->run();
|
|
my $filename = $save_dialog->get_filename();
|
|
$save_dialog->destroy();
|
|
|
|
if ($save_response eq 'ok' && defined $filename) {
|
|
save_file($filename);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
} elsif ($response eq 'no') {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub show_error_dialog {
|
|
my ($message) = @_;
|
|
my $dialog = Gtk3::MessageDialog->new(
|
|
$window,
|
|
'modal',
|
|
'error',
|
|
'ok',
|
|
$message
|
|
);
|
|
$dialog->run();
|
|
$dialog->destroy();
|
|
}
|
|
|
|
sub create_search_dialog {
|
|
return if defined $search_dialog;
|
|
|
|
$search_dialog = Gtk3::Dialog->new();
|
|
$search_dialog->set_title('Find');
|
|
$search_dialog->set_transient_for($window);
|
|
$search_dialog->add_button('Close', 'close');
|
|
$search_dialog->add_button('Find Next', 'ok');
|
|
|
|
my $content_area = $search_dialog->get_content_area();
|
|
my $vbox = Gtk3::Box->new('vertical', 5);
|
|
$content_area->add($vbox);
|
|
|
|
my $hbox = Gtk3::Box->new('horizontal', 5);
|
|
$vbox->pack_start($hbox, 0, 0, 0);
|
|
|
|
$hbox->pack_start(Gtk3::Label->new('Search for:'), 0, 0, 0);
|
|
my $entry = Gtk3::Entry->new();
|
|
$hbox->pack_start($entry, 1, 1, 0);
|
|
|
|
my $case_sensitive = Gtk3::CheckButton->new_with_label('Case sensitive');
|
|
$vbox->pack_start($case_sensitive, 0, 0, 0);
|
|
|
|
$search_dialog->show_all();
|
|
|
|
$search_dialog->signal_connect(response => sub {
|
|
my ($dialog, $response) = @_;
|
|
if ($response eq 'ok') {
|
|
find_text($entry->get_text(), $case_sensitive->get_active());
|
|
} elsif ($response eq 'close') {
|
|
$dialog->hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
sub create_replace_dialog {
|
|
return if defined $replace_dialog;
|
|
|
|
$replace_dialog = Gtk3::Dialog->new();
|
|
$replace_dialog->set_title('Find and Replace');
|
|
$replace_dialog->set_transient_for($window);
|
|
$replace_dialog->add_button('Close', 'close');
|
|
$replace_dialog->add_button('Replace', 'ok');
|
|
$replace_dialog->add_button('Replace All', 'apply');
|
|
|
|
my $content_area = $replace_dialog->get_content_area();
|
|
my $vbox = Gtk3::Box->new('vertical', 5);
|
|
$content_area->add($vbox);
|
|
|
|
# Find entry
|
|
my $find_box = Gtk3::Box->new('horizontal', 5);
|
|
$vbox->pack_start($find_box, 0, 0, 0);
|
|
$find_box->pack_start(Gtk3::Label->new('Find:'), 0, 0, 0);
|
|
my $find_entry = Gtk3::Entry->new();
|
|
$find_box->pack_start($find_entry, 1, 1, 0);
|
|
|
|
# Replace entry
|
|
my $replace_box = Gtk3::Box->new('horizontal', 5);
|
|
$vbox->pack_start($replace_box, 0, 0, 0);
|
|
$replace_box->pack_start(Gtk3::Label->new('Replace with:'), 0, 0, 0);
|
|
my $replace_entry = Gtk3::Entry->new();
|
|
$replace_box->pack_start($replace_entry, 1, 1, 0);
|
|
|
|
my $case_sensitive = Gtk3::CheckButton->new_with_label('Case sensitive');
|
|
$vbox->pack_start($case_sensitive, 0, 0, 0);
|
|
|
|
$replace_dialog->show_all();
|
|
|
|
$replace_dialog->signal_connect(response => sub {
|
|
my ($dialog, $response) = @_;
|
|
if ($response eq 'ok') {
|
|
replace_next($find_entry->get_text(), $replace_entry->get_text(), $case_sensitive->get_active());
|
|
} elsif ($response eq 'apply') {
|
|
replace_all($find_entry->get_text(), $replace_entry->get_text(), $case_sensitive->get_active());
|
|
} elsif ($response eq 'close') {
|
|
$dialog->hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
sub find_text {
|
|
my ($search_text, $case_sensitive) = @_;
|
|
return unless $search_text;
|
|
|
|
my $buffer = $text_view->get_buffer();
|
|
my $start_iter = $buffer->get_iter_at_mark($buffer->get_insert());
|
|
|
|
# Start from beginning if we're at the end or no previous search
|
|
if (!defined $last_search_pos || !$start_iter->forward_char()) {
|
|
$start_iter = $buffer->get_start_iter();
|
|
}
|
|
|
|
my $found;
|
|
if ($case_sensitive) {
|
|
$found = $start_iter->forward_search($search_text, 'text-only', undef);
|
|
} else {
|
|
$found = $start_iter->forward_search($search_text, ['text-only', 'case-insensitive'], undef);
|
|
}
|
|
|
|
if ($found) {
|
|
my ($match_start, $match_end) = @$found;
|
|
$buffer->select_range($match_start, $match_end);
|
|
$text_view->scroll_to_iter($match_start, 0.0, 1, 0.0, 0.5);
|
|
$last_search_pos = $match_end;
|
|
return 1;
|
|
} else {
|
|
# If not found from current position, try from start
|
|
if ($start_iter->get_offset() > 0) {
|
|
$last_search_pos = undef;
|
|
return find_text($search_text, $case_sensitive);
|
|
}
|
|
show_error_dialog("Text not found: $search_text");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub replace_next {
|
|
my ($find_text, $replace_text, $case_sensitive) = @_;
|
|
return unless $find_text;
|
|
|
|
if (find_text($find_text, $case_sensitive)) {
|
|
my $buffer = $text_view->get_buffer();
|
|
$buffer->delete_selection(1, 1);
|
|
$buffer->insert_at_cursor($replace_text);
|
|
$last_search_pos = undef; # Force next search from current position
|
|
}
|
|
}
|
|
|
|
sub replace_all {
|
|
my ($find_text, $replace_text, $case_sensitive) = @_;
|
|
return unless $find_text;
|
|
|
|
my $count = 0;
|
|
$last_search_pos = undef;
|
|
|
|
while (find_text($find_text, $case_sensitive)) {
|
|
my $buffer = $text_view->get_buffer();
|
|
$buffer->delete_selection(1, 1);
|
|
$buffer->insert_at_cursor($replace_text);
|
|
$last_search_pos = undef;
|
|
$count++;
|
|
}
|
|
|
|
show_info_dialog("Replaced $count occurrence" . ($count == 1 ? '' : 's'));
|
|
}
|
|
|
|
sub update_cursor_position {
|
|
my $buffer = $text_view->get_buffer();
|
|
my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
|
|
my $line = $iter->get_line() + 1;
|
|
my $column = $iter->get_line_offset() + 1;
|
|
$status_bar->push(0, "Line: $line, Column: $column");
|
|
}
|
|
|
|
sub show_info_dialog {
|
|
my ($message) = @_;
|
|
my $dialog = Gtk3::MessageDialog->new(
|
|
$window,
|
|
'modal',
|
|
'info',
|
|
'ok',
|
|
$message
|
|
);
|
|
$dialog->run();
|
|
$dialog->destroy();
|
|
}
|
|
|
|
# Connect window signals
|
|
$window->signal_connect(delete_event => sub {
|
|
if (!check_save_changes()) {
|
|
return 1; # Cancel the close
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
$window->signal_connect(destroy => sub { Gtk3::main_quit() });
|
|
$exit_item->signal_connect(activate => sub { $window->destroy() });
|
|
|
|
# Show all widgets
|
|
$window->show_all();
|
|
|
|
# Add these new functions at the end before Gtk3::main()
|
|
sub handle_key_press {
|
|
my ($widget, $event) = @_;
|
|
my $keyval = $event->keyval;
|
|
my $state = $event->state;
|
|
|
|
# Check if Control key is pressed
|
|
if ($state & CONTROL_MASK) {
|
|
if (chr($keyval) eq 'z' || chr($keyval) eq 'Z') {
|
|
undo_action();
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'y' || chr($keyval) eq 'Y') {
|
|
redo_action();
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'c' || chr($keyval) eq 'C') {
|
|
$copy_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'x' || chr($keyval) eq 'X') {
|
|
$cut_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
elsif (chr($keyval) eq 'v' || chr($keyval) eq 'V') {
|
|
$paste_item->signal_emit('activate');
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub buffer_changed_cb {
|
|
return if $ignore_changes;
|
|
|
|
my $content = $text_buffer->get_text(
|
|
$text_buffer->get_start_iter(),
|
|
$text_buffer->get_end_iter(),
|
|
1
|
|
);
|
|
|
|
push @undo_stack, $content;
|
|
@redo_stack = (); # Clear redo stack when new changes occur
|
|
}
|
|
|
|
sub undo_action {
|
|
return unless @undo_stack;
|
|
|
|
my $current_content = $text_buffer->get_text(
|
|
$text_buffer->get_start_iter(),
|
|
$text_buffer->get_end_iter(),
|
|
1
|
|
);
|
|
|
|
push @redo_stack, $current_content;
|
|
|
|
$ignore_changes = 1;
|
|
my $prev_content = pop @undo_stack;
|
|
$text_buffer->set_text($prev_content);
|
|
$ignore_changes = 0;
|
|
}
|
|
|
|
sub redo_action {
|
|
return unless @redo_stack;
|
|
|
|
my $current_content = $text_buffer->get_text(
|
|
$text_buffer->get_start_iter(),
|
|
$text_buffer->get_end_iter(),
|
|
1
|
|
);
|
|
|
|
push @undo_stack, $current_content;
|
|
|
|
$ignore_changes = 1;
|
|
my $next_content = pop @redo_stack;
|
|
$text_buffer->set_text($next_content);
|
|
$ignore_changes = 0;
|
|
}
|
|
|
|
# Start the main loop
|
|
Gtk3::main();
|