Commits

Herbert Breunung committed e0081b5

expand range of included modules, preparing the use of docbar

Comments (0)

Files changed (14)

doc/CompleteProgramming.pod

Empty file added.

lib/Kephra/API.pm

 use v5.12;
 use warnings;
 
+use Kephra::API::Command;
+use Kephra::API::DocumentStash;
+use Kephra::API::Event;
+use Kephra::API::KeyMap;
+#use Kephra::API::Macro;
+#use Kephra::API::Plugin;
+#use Kephra::API::Sandrum;
+use Kephra::App::Focus;
+
 package Kephra::API;
+my $VERSION = 0.5;
 
-use Kephra::API::KeyMap;
+#Kephra::API::Command::switch_variables({
+ #'$app'    => { switch => 'app',         class => 'Kephra::App' },
+ #'$win'    => { switch => 'main_window', class => 'Kephra::App::Window' },
+ #'$docbar' => { switch => 'docbar',      class => 'Kephra::App::Bar::Document' },
+ #'$doc'    => { switch => 'document',    class => 'Kephra::Document' },
+ #'$ed'     => { switch => 'editor',      class => 'Kephra::App::Editor' },
+#});
 
-1;
+#sub app                 { $Kephra::App::_ref }
+#sub main_window         { $Kephra::App::Window::_ref }
+#sub docbar              { Kephra::App::Part::Editor::docbar() }
+#sub all_docbars         { Kephra::App::Part::Editor::all_docbars() }
+#sub passive_docbars     { Kephra::App::Part::Editor::passive_docbars() }
+#sub document            { Kephra::API::Doc::active() }
+#sub all_documents       { Kephra::API::Doc::all() }
+#sub do_with_all_editors {&Kephra::API::Doc::do_with_all_editors }
+#sub editor              { Kephra::API::Doc::active_editor() }
+
+sub focus           { Kephra::App::Focus::set(@_) }
+sub focus_back      { Kephra::App::Focus::set_back(@_) }
+
+# IO-unit
+#sub input  {main_window()->{'part'}{'iounit'}->get_line() if ref main_window() and exists main_window()->{'part'}{'iounit'} }
+#sub output {main_window()->{'part'}{'iounit'}->output(@_) if ref main_window() and exists main_window()->{'part'}{'iounit'} }
+#sub log    {main_window()->{'part'}{'iounit'}->output('log', @_) if ref main_window() and exists main_window()->{'part'}{'iounit'} }
+
+#sub configs  { }
+
+# command list
+#sub add_command { }
+#sub del_command { }
+#sub run_command { }
+#sub command_status {}
+#
+# cmd line lang
+#sub run_sandrum_commands {}
+#
+# event table
+#sub add_event_callback { }
+#sub del_event_callback { }
+#sub trigger_event      { }
+#sub freeze_event       { }
+#sub thaw_event         { }
+#
+# plugin API
+#sub register_plugin {}
+#sub get_plugin_status {}
+#sub get_plugin_data {}
+#sub available_plugins {}
+#sub unregister_plugin {}
+
+
+1; # if you want to know more, please read Kephra::Internals.pod

lib/Kephra/API/Command.pm

+use v5.12;
+use warnings;
+
+package Kephra::API::Command;                             # callable by the user
+
+my %list;     # by ID
+#my %keyproxy;# by keycode
+my %switchvar; #switchvar vor cmd definitions
+my $done_init = 0;
+
+
+sub      _raw_list { \%list }
+sub sub_exists     { no strict 'refs'; !!*{ $_[0] }{CODE} if $_[0] }
+sub package_exists { no strict 'refs'; !!%{ $_[0] . '::' } if $_[0] }
+sub calling_module { ( caller(1) )[0] }
+
+sub switch_variables {
+	my ($var_def) = shift;
+	my $api = 'Kephra::API';
+	my $caller = calling_module();
+	return Kephra::Log::error("only callable by $api", 1) if $caller ne $api;
+	#return Kephra::Log::error("call me just once", 1) if keys %switchvar;
+	return Kephra::Log::error("need a hashref", 1) unless ref $var_def eq 'HASH';
+	for my $var (keys %$var_def){
+		delete $var_def->{$var} unless substr($var, 0, 1) eq '$';
+		delete $var_def->{$var} unless sub_exists( $api .'::'. $var_def->{$var}{'switch'} );
+		delete $var_def->{$var} unless package_exists ( $var_def->{$var}{'class'} );
+	}
+	%switchvar = %$var_def;
+	compile( keys %list );
+	$done_init = 1;
+}
+
+sub register {
+	my $cmd = shift;
+	return Kephra::Log::error("cmd def have to be in a hash ref, not $cmd", 1)
+		unless ref $cmd eq 'HASH';
+	my $caller = calling_module();
+	for my $ID (keys %$cmd) {
+		Kephra::Log::warning( 
+			"$cmd already registered to do ". property($ID, 'sub')." by ".
+			property($ID, 'source')  ), next if registered( $ID );
+		#for (qw/sub options state event label help keys icon/){} filter input later
+		$list{$ID}           = $cmd->{$ID};
+		$list{$ID}{'source'} = $caller;
+		compile($ID) if $done_init;
+	}
+}
+
+sub compile {
+	for my $cmd_ID (@_) {
+		my $cmd = $list{ $cmd_ID };
+		my $sub = $cmd->{'sub'};
+		Kephra::Log::warning( "$cmd_ID lacks value on hashkey 'sub'"), next unless $sub;
+
+		if ( substr($sub, 0, 1) eq '$'){
+			my $method_pos = index($sub, '->');
+			my $var = substr($sub, 0, $method_pos);
+			Kephra::Log::warning
+				("unknown switchvar $var in $cmd_ID 'sub' entry", 1), next
+					unless ref $switchvar{ $var };
+			my $method = $switchvar{$var}{'class'}.'::'.substr($sub, $method_pos+2);
+			# move that check into test suite
+			#Kephra::Log::warning ("called unknown method with $sub", 1), next
+			#	unless sub_exists($method) ;
+			
+			$cmd->{'coderef'} = 'Kephra::API::' . $switchvar{$var}{'switch'} . '()' .
+			                      substr($sub, $method_pos);
+		}
+		elsif ( index($sub, '::') == -1) {
+			$cmd->{'coderef'} = $cmd->{'source'} . '::' . $sub
+		}
+		else { $cmd->{'coderef'} = $sub }
+
+		# add parameter if are any
+		$cmd->{'coderef'} .= exists $cmd->{'parameter'} # just one parameter yet
+			? "( '" . $cmd->{'parameter'} . "' )"
+			: '()';
+		$cmd->{'coderef'} = eval 'sub { ' . $cmd->{'coderef'} . ' }';
+
+		Kephra::API::KeyMap::register_keys({ $cmd_ID => $cmd->{'keys'} }) if $cmd->{'keys'};
+	}
+}
+
+
+sub run {
+	my $cmd = shift;
+	$cmd = [$cmd] unless ref $cmd eq ref [];
+	my $return;
+	for (@$cmd) { $return = $list{$_}{'coderef'}->() if exists $list{$_} }
+	$return;
+}
+sub run_by_keycode {
+	my ($code, $map) = @_;
+}
+
+sub registered      { 1 if defined $_[0]  and exists $list{ $_[0] }             }
+sub property_exists { 1 if registered($_[0]) and exists $list{ $_[0] }{ $_[1] } }
+sub add {}
+
+sub all_properties { 
+	if ( registered($_[0]) ) { $list{$_[0]} }
+	else { Kephra::Log::warning('requested data of unknown command '.$_[0], 1) }
+}
+sub property       { $list{$_[0]}{$_[1]}         if property_exists(@_) }
+sub set_property   { $list{$_[0]}{$_[1]} = $_[2] if property_exists(@_) }
+
+
+1;
+
+__END__
+
+=head1 Command Definition
+
+cmd_ID => {
+	coderef   => compiled from sub, source and option, saves state
+	sub       => 'Kephra::File::new',
+	parameter => [],
+	source    => package that registered that cmd
+	state     => return value of the call or antother coderef
+	event     => '',
+	label     => 'New',
+	help      => '', # help text to be shown in statusbar or as popup
+	keys      => 'Ctrl + N',
+	icon      => '',
+	bitmap    => '',  #Wx::Bitmap Object
+}

lib/Kephra/API/DocStash.pm

Empty file removed.

lib/Kephra/API/DocumentStash.pm

+use v5.12;
+use warnings;
+use Wx;
+use Kephra::App::Editor;
+use Kephra::App::Panel;
+use Kephra::Document;
+
+package Kephra::API::DocumentStash;
+my %document = ('ID'=>{});             # keys of subhashes: ID editor panel file
+my $active;
+my $lowest_free_ID = 1;
+my $lowest_free_anon_NR = 1;
+
+use constant DOC_CLASS => 'Kephra::Document';
+use Scalar::Util qw(blessed looks_like_number);
+
+
+sub active        { $active || '' }
+sub active_editor { $active->editor->{Kephra::API::docbar()} if $active }
+sub active_panel  { $active->panel->{Kephra::API::docbar()} if $active }
+sub set_active {
+	my $doc = find( shift );
+	$active = $doc if $doc;
+}
+sub previous      { App::Kephra::Focus::last( DOC_CLASS ) }
+sub all           { values %{$document{'ID'}} }
+
+sub is { return 1 if blessed($_[0]) and $_[0]->isa( DOC_CLASS ); return 0 }
+
+
+sub add {
+	my $doc = shift;
+	Kephra::Log::warning('need a ' . DOC_CLASS . " thats in stash, not $doc!", 1)
+		unless is($doc);
+	$document{'ID'}  {$lowest_free_ID} = $doc;
+	add_instance($doc, $_) for keys $doc->editor;
+	if ($doc->file_path) {
+		$document{'file'}{$doc->file_path} = $doc;
+	} else {
+		$doc->anon_NR( $lowest_free_anon_NR++ );
+		$document{'anon'}{$doc->anon_NR} = $doc;
+	}
+	$lowest_free_ID++;
+}
+
+
+sub rename_file {
+	my $doc = find( shift );
+	Kephra::Log::warning('need a ' . DOC_CLASS . " thats in stash, not $doc!", 1)
+		unless is($doc);
+	my $file = $doc->prev_file_path;
+	delete $document{'file'}{$file} if $file and $document{'file'}{$file};
+	if ($doc->file_path){
+		$document{'file'}{$doc->file_path} = $doc;
+		unless ($doc->prev_file_path) {
+			delete $document{'anon'}{$doc->anon_NR};
+			$doc->anon_NR(0);
+		}
+	} else {
+		if ($doc->prev_file_path) {
+			$doc->anon_NR( $lowest_free_anon_NR++ );
+			$document{'anon'}{$doc->anon_NR} = $doc;
+		}
+	}
+}
+
+
+sub remove {
+	my $doc = shift;
+#say "remove $doc ", scalar keys %{$document{'ID'}}, ' ',scalar keys %{$document{'file'}}; 
+	Kephra::Log::warning('need a ' . DOC_CLASS . ' thats in stash, not $doc!', 1)
+		unless is($doc);
+	delete $document{'ID'}{$doc->ID} if $doc->ID;
+	#remove_instance($doc, $_) for values $doc->panel;
+	delete $document{'file'}{$doc->file_path} if $doc->file_path;
+#say "removed $doc ", scalar keys %{$document{'ID'}}, ' ',scalar keys %{$document{'file'}}; 
+}
+
+
+sub add_instance {
+	my ($doc, $bar) = @_;
+	return Kephra::Log::warning('need as first parameter a'.DOC_CLASS.' thats in stash, not $doc!', 1)
+		unless is($doc);
+	#return Kephra::Log::warning("need a Kephra::App::Bar::Document instance as second parameter, not $bar")
+	#	unless blessed($bar) eq 'Kephra::App::Bar::Document';
+	$document{'panel'} { $doc->panel->{$bar}  } = $doc;
+	$document{'editor'}{ $doc->editor->{$bar} } = $doc;
+}
+
+
+sub remove_instance {
+	my ($doc, $bar) = @_;
+	return Kephra::Log::warning('need as first parameter a'.DOC_CLASS.' thats in stash, not $doc!', 1)
+		unless is($doc);
+	return Kephra::Log::warning("need a Kephra::App::Bar::Document instance as second parameter, not $bar")
+		unless blessed($bar) eq 'Kephra::App::Bar::Document';
+	delete $document{'panel'}{ $doc->panel->{$bar} };
+	delete $document{'editor'}{ $doc->editor->{$bar} };
+}
+
+
+sub find {
+	my $any = shift;
+	Kephra::Log::warning("need a parameter!", 1) unless defined $any and $any;
+	return     looks_like_number($any)                                     ? $document{'ID'}    {$any}
+	         : ref($any) eq ''                                             ? $document{'file'}  {$any}
+	         : $any->isa('Kephra::App::Panel')                             ? $document{'panel'} {$any}
+	         : $any->isa('Kephra::App::Editor')                            ? $document{'editor'}{$any}
+	         :($any->isa(DOC_CLASS) and $document{'ID'}{$any->ID} eq $any) ? $any
+	         :                                                               undef;
+}
+
+
+sub file_loaded {
+	my $file_path = shift;
+	#Kephra::Log::warning("needs a file path as input!") unless $file_path and -e $file_path;
+	for my $doc ( all() ) {
+		return 1 if $doc->file_path and $doc->file_path eq $file_path;
+	}
+}
+
+
+sub all_editors { keys %{$document{'editor'}} }
+sub do_with_all_editors {
+}
+
+1;

lib/Kephra/API/Event.pm

+use strict;
+use warnings;
+use Kephra::API;
+
+package Kephra::API::Event;
+
+my %event;
+
+sub add_callback {}
+sub del_callback {}
+sub trigger_callback {}
+sub freeze_callback {}
+sub thaw_callback{}
+
+sub add_event {}
+sub del_event {}
+sub trigger_event {}
+sub freeze_event {}
+sub thaw_event{}
+
+sub global_halt {}
+sub global_restore {}
+
+
+1;

lib/Kephra/App/Bar/Document.pm

+use v5.12;
+use warnings;
+use Wx;
+use Wx::AUI;
+use Kephra::API;
+use Kephra::API::Command;
+
+package Kephra::App::Bar::Document;
+our @ISA = 'Wx::AuiNotebook';
+our $_ref;
+
+use Scalar::Util qw(blessed looks_like_number);
+
+
+#Kephra::API::Command::register({
+ #'docbar-select-left'     =>{sub=>'$docbar->select_page_left',     label=>'Previous Tab',  keys=>'ctrl+pageup'},
+ #'docbar-select-right'    =>{sub=>'$docbar->select_page_right',    label=>'Next Tab',      keys=>'ctrl+pagedown'},
+ #'docbar-select-leftmost'=>{sub=>'$docbar->select_page_leftmost', label=>'First Tab',     keys=>'ctrl+shift+pageup'},
+ #'docbar-select-rightmost'=>{sub=>'$docbar->select_page_rightmost',label=>'Last Tab',      keys=>'ctrl+shift+pagedown'},
+ #'docbar-move-left'       =>{sub=>'$docbar->move_page_left',       label=>'Move Left',     keys=>'ctrl+shift+pageup'},
+ #'docbar-move-right'      =>{sub=>'$docbar->move_page_right',      label=>'Move Right',    keys=>'ctrl+shift+pagedown'},
+ #'docbar-move-leftmost'   =>{sub=>'$docbar->move_page_leftmost',   label=>'Move Leftmost', keys=>'alt+shift+pageup'},
+ #'docbar-move-rightmost'  =>{sub=>'$docbar->move_page_rightmost',  label=>'Move Rightmost',keys=>'alt+shift+pagedown'},
+#});
+
+
+sub new {
+	my( $class, $parent) = @_;
+	my $self = $_ref = $class->SUPER::new( $parent, -1, [-1,-1], [-1,-1],
+		&Wx::wxAUI_NB_TOP | &Wx::wxAUI_NB_TAB_MOVE | &Wx::wxAUI_NB_WINDOWLIST_BUTTON |
+		&Wx::wxAUI_NB_SCROLL_BUTTONS | &Wx::wxAUI_NB_CLOSE_ON_ACTIVE_TAB
+	);
+
+	$self->{'visual_page_order'} = [];   # visual order of internal pos : vis -> int
+	$self->{'internal_page_order'} = []; # internal order of visual pos : int -> vis
+
+	$_->add_instance($self) for Kephra::API::all_documents();
+	$self->mount_events();
+
+	$self;
+}
+
+sub Destroy {
+	my ($self) = @_;
+	$_->del_instance($self) for Kephra::API::all_documents();
+	$self->SUPER::Destroy( );
+	1;
+}
+
+sub mount_events {
+	my ($self) = @_;
+	Wx::Event::EVT_SET_FOCUS ($self,  sub { print "focus--\n";$_[1]->Skip });
+	Wx::Event::EVT_AUINOTEBOOK_BEGIN_DRAG( $self, -1, sub {
+		my ($bar, $event ) = @_;
+		$bar->{'DND_page_nr'} = $event->GetSelection;
+	});
+	Wx::Event::EVT_AUINOTEBOOK_END_DRAG($self, -1, sub {
+		my ($bar, $event ) = @_;
+		return unless defined $bar->{'DND_page_nr'};
+		$bar->move_page_position_visually($bar->{'DND_page_nr'}, $event->GetSelection);
+		$bar->{'DND_page_nr'} = undef;
+		Kephra::App::Focus::stay();
+	});
+	Wx::Event::EVT_AUINOTEBOOK_PAGE_CHANGED( $self, -1, sub {
+		my ($bar, $event ) = @_;
+		my $new_page = $bar->GetPage( $event->GetSelection );
+		Kephra::API::Doc::set_active( $new_page );
+		Kephra::API::main_window()->refresh_title();
+		Kephra::API::focus( $new_page );
+		$event->Skip;
+	});
+	Wx::Event::EVT_AUINOTEBOOK_PAGE_CLOSE( $self, -1, sub {
+		my ($bar, $event ) = @_; 
+		$event->Veto;
+		Kephra::File::close_active();
+	});
+	 keep focus on editor even when klicking on tab bar
+	Wx::Event::EVT_LEFT_DOWN( $self, sub { Kephra::API::active_editor()->focus; print "--down"; });
+	$self->Connect( -1, -1, &Wx::wxEVT_SET_FOCUS, sub {  print "--"; } )
+	Wx::Event::EVT_SET_FOCUS( $self, sub { my ($bar, $event ) = @_; $event->Skip; say "--bar"; });
+}
+
+sub unmount_events {
+	my ($self) = @_;
+	Wx::Event::EVT_AUINOTEBOOK_BEGIN_DRAG( $self, -1, sub {});
+	Wx::Event::EVT_AUINOTEBOOK_END_DRAG  ( $self, -1, sub {});
+	Wx::Event::EVT_AUINOTEBOOK_PAGE_CHANGED( $self, -1, sub {});
+	Wx::Event::EVT_AUINOTEBOOK_PAGE_CLOSE  ( $self, -1, sub {});
+}
+
+
+#sub add_page {
+	#my ($self, $new_page, $position, $title, $set_active) = @_;
+	#return Kephra::Log::warning( "got no panel", 1 ) until Kephra::App::Util::is_panel($new_page);
+	#my $active_pos = $self->GetSelection;
+#
+	#$title    = ''                           unless defined $title;
+	#$position = 'right'                      if $position eq 'default' or $position eq -1;
+	#$position = $self->rightmost_page_pos+1  if $position eq 'rightmost';
+	#$position = $active_pos + 1              if $position eq 'right';
+	#$position = $active_pos                  if $position eq 'left';
+	#$position = $self->leftmost_page_pos     if $position eq 'leftmost';
+	#$set_active = 0                          unless defined $set_active;
+#
+	#$new_page->Reparent($self);
+	#$self->InsertPage( $position, $new_page, $title, $set_active);
+	#$self->set_page_title( $title, $new_page );
+	#Kephra::API::focus($new_page) if $set_active;
+#
+	 #inserting new index to position translators
+	#for   (@{$self->{'visual_page_order'}}){ $_++ if $_ >= $position }
+	#splice @{$self->{'visual_page_order'}},  $position, 0, $position;
+	#$self->refresh_internal_page_order();
+#
+	#$self;
+#}
+
+#sub remove_page {
+	#my ($self, $page) = @_;
+	#my $internal_position = $self->GetPageIndex( $page );
+#
+	#$self->RemovePage( $internal_position );
+	#my $visual_position = $self->{'internal_page_order'}[$internal_position];
+	#my $visual = $self->{'visual_page_order'};
+	#splice @$visual, $visual_position, 1;
+	#for (@$visual) {$_-- if $_ >= $internal_position}
+	#$self->{'visual_page_order'} = $visual;
+	#$self->refresh_internal_page_order;
+#}
+#
+#
+#sub refresh_internal_page_order {       # sync visual_page_order index with internal_page_order after each change
+	#my ($self) = @_;
+	#my $visual = $self->{'visual_page_order'};
+	#return unless ref $visual eq ref [];
+	#my $internal;
+	#$internal->[ $visual->[$_] ] = $_ for 0 .. @$visual-1;
+	#$self->{'internal_page_order'} = $internal;
+#}
+#
+#sub move_page_position_visually {          # for dnd only
+	#my ($self, $from, $to ) = @_;
+	#return unless $from >= 0 and $from < $self->GetPageCount;
+	#return unless $to >= 0 and $to < $self->GetPageCount;
+	#my $position = splice @{$self->{'visual_page_order'}}, $from, 1;
+	#splice @{$self->{'visual_page_order'}}, $to, 0, $position;
+	#$self->refresh_internal_page_order();
+#}
+#
+#sub move_page_visually  {               # for movements by keyboard
+	#my ($self, $from, $to ) = @_;
+	#my $max = $self->GetPageCount - 1;
+	#return if $from < 0 or $from > $max;
+	#return if $to < 0 or $to > $max;
+	#return if $from == $to;
+#
+	#my $pos = $self->{'visual_page_order'}[ $from ];
+	#my $page = $self->GetPage( $pos );
+	#my $label = $self->GetPageText( $pos );
+	#my $visual = $self->{'visual_page_order'};
+#
+	#$self->unmount_events();
+	#$self->RemovePage( $pos );
+	#$self->InsertPage( $to, $page, $label);
+	#my $removed = splice @$visual, $from, 1;
+	#if ($from < $to) { for (@$visual) {$_-- if $_ > $removed and $_ <= $to} }
+	#else             { for (@$visual) {$_++ if $_ < $removed and $_ >= $to} }
+	#splice @$visual, $to, 0, $to;
+	#$self->{'visual_page_order'} = $visual;
+	#$self->refresh_internal_page_order();
+	#$self->SetSelection( $self->{'visual_page_order'}[$to] );
+	#$self->mount_events();
+#}
+#
+#sub move_page_left      {
+	#my ($self) = @_;
+	#$self->move_page_visually ( 
+		#$self->active_visual_pos, $self->next_page_pos_rot_left( $self->GetSelection )
+	#);
+#}
+#sub move_page_right     {
+	#my ($self) = @_;
+	#$self->move_page_visually( 
+		#$self->active_visual_pos, $self->next_page_pos_rot_right( $self->GetSelection )
+	#);
+#}
+#sub move_page_leftmost  { 
+	#my ($self) = @_;
+	#$self->move_page_visually( $self->active_visual_pos, $self->leftmost_page_pos );
+#}
+#sub move_page_rightmost { 
+	#my ($self) = @_;
+	#$self->move_page_visually( $self->active_visual_pos, $self->rightmost_page_pos );
+#}
+#
+#sub select_page_left    { 
+	#my ($self) = @_;
+	#$self->raise_page( $self->next_page_pos_rot_left( $self->GetSelection ) );
+#}
+#sub select_page_right   { 
+	#my ($self) = @_;
+	#$self->raise_page( $self->next_page_pos_rot_right( $self->GetSelection ) );
+#}
+#sub select_page_leftmost  { $_[0]->raise_page( $_[0]->leftmost_page_pos ) }
+#sub select_page_rightmost { $_[0]->raise_page( $_[0]->rightmost_page_pos ) }
+
+#sub active_visual_pos     { $_[0]->{'internal_page_order'}[ $_[0]->GetSelection ] }
+#sub leftmost_page_pos     { 0 }
+#sub rightmost_page_pos    { $_[0]->GetPageCount-1 }
+#sub valid_page_pos        { 
+	#1 if $_[1] >= $_[0]->leftmost_page_pos and $_[1]<= $_[0]->rightmost_page_pos
+#}
+#sub next_page_pos_rot_left{
+	#my ($self) = @_; # take in position of internal order
+	#my $pos = $self->{'internal_page_order'}[ $_[1] ];
+	#$self->{'visual_page_order'}[$pos == 0 ? $self->rightmost_page_pos : $pos-1]
+#}
+#sub next_page_pos_rot_right{
+	#my ($self) = @_; # take in position of internal order
+	#my $pos = $self->{'internal_page_order'}[ $_[1] ];
+	#$self->{'visual_page_order'}[$pos == $self->rightmost_page_pos ? 0 : $pos+1]
+#}
+
+
+#sub raise_page    {
+	#my ($self, $pop) = @_; # can be Position Or Page (panel reference)
+	#my $position = int $pop eq $pop ? $pop : $self->GetPageIndex($pop);
+	#return unless $self->valid_page_pos( $position );
+	# if just selecting the currrent, only tab drives focus nuts
+	#$self->SetSelection( $position ) unless $position == $self->GetSelection;
+	#my $page = $self->GetPage($position);
+	#my $doc = Kephra::API::Doc::find($page);
+	#Kephra::API::Doc::set_active( $doc );
+	#Kephra::API::focus ( defined $doc ? $doc->editor->{$self} : $page );
+	#Kephra::API::main_window()->refresh_title();
+#
+#}
+
+
+#sub active_title { $_[0]->GetPageText( $_[0]->GetSelection ) }
+
+#sub set_page_title {
+	#my ($self, $label, $page) = @_;
+	#$page = $self->GetSelection unless defined $page;
+	#return Kephra::Log::warning("need a Kephra::App::Panel or valid position number") 
+		#unless (looks_like_number($page) and $self->valid_page_pos($page))
+		#or (blessed($page) and $page->isa('Kephra::App::Panel'));
+	#my $found = $self->GetPageIndex($page);
+	#my $position = $found eq &Wx::wxNOT_FOUND ? $page : $found;
+	#$self->SetPageText( $position, $label );
+#}
+
+
+1;

lib/Kephra/App/Dialog.pm

+use v5.10;
+use strict;
+use warnings;
+use Wx;
+use Kephra::API;
+
+package Kephra::App::Dialog;
+
+Kephra::API::Command::register({
+ 'config-global-dialog'=>{sub=>'config',       label=>'Dialog',       keys=> 'ctrl+alt+c'},
+ 'config-keymap-dialog'=>{sub=>'keymap',       label=>'Keymapping',   keys=> 'ctrl+alt+k'},
+});
+
+
+sub _parent { Kephra::API::main_window() }
+
+# standard dialogs
+
+sub message {
+	$_[1] = $_[1] || 'Kephra Message'; 
+	splice @_, 2, 0, &Wx::wxOK | &Wx::wxSTAY_ON_TOP;
+	_box( @_ );
+}
+sub info   {
+	$_[1] = $_[1] || 'Kephra Information'; 
+	splice @_, 2, 0, &Wx::wxOK | &Wx::wxICON_INFORMATION | &Wx::wxSTAY_ON_TOP;
+	_box( @_ );
+}
+sub warning {
+	$_[1] = $_[1] || 'Kephra Warning'; 
+	splice @_, 2, 0, &Wx::wxOK | &Wx::wxICON_WARNING | &Wx::wxSTAY_ON_TOP;
+	_box( @_ );
+}
+sub yes_no     { 
+	$_[1] = $_[1] || 'Kephra Question'; 
+	splice @_, 2, 0, &Wx::wxYES_NO | &Wx::wxICON_QUESTION | &Wx::wxSTAY_ON_TOP;
+	_box( @_ );
+}
+sub yes_no_cancel{
+	$_[1] = $_[1] || 'Kephra Question'; 
+	splice @_, 2, 0, &Wx::wxYES_NO | &Wx::wxCANCEL | &Wx::wxICON_QUESTION | &Wx::wxSTAY_ON_TOP;
+	_box( @_ );
+}
+sub _box {                                   # $message, $title, $style, $parent
+	Kephra::Log::warning('need at least a message as first parameter', 2) unless $_[0];
+	$_[3] = $_[3] || _parent(); 
+	Wx::MessageBox( @_ );
+}
+
+sub get_file_open {
+	my $title  = shift // 'Open File ...';
+	my $dir    = shift // '.';
+	my $filter = shift // '(*)|*';
+	my $parent = shift // _parent();
+	Wx::FileSelector( $title, $dir, '', '', $filter, &Wx::wxFD_OPEN, $parent);
+}
+
+sub get_files_open {
+	my $title  = shift // 'Open File ...';
+	my $dir    = shift // '.';
+	my $filter = shift // '(*)|*';
+	my $parent = shift // _parent();
+	my $dialog = Wx::FileDialog->new
+		($parent, $title, $dir, '', $filter, &Wx::wxFD_OPEN | &Wx::wxFD_MULTIPLE);
+	if ($dialog->ShowModal != &Wx::wxID_CANCEL) {
+		return $dialog->GetPaths;
+	}
+}
+
+sub get_file_save { 
+	my $title  = shift // 'Save File As ...';
+	my $dir    = shift // '.';
+	my $filter = shift // '(*)|*';
+	my $parent = shift // _parent();
+	Wx::FileSelector( $title, $dir, '', '', $filter, &Wx::wxFD_SAVE, $parent);
+}
+
+sub get_dir  {  Wx::DirSelector      ( @_[0,1], 0, [-1,-1], _parent()) }
+sub get_font {  Wx::GetFontFromUser  ( _parent(), $_[0]) }
+sub get_text {  Wx::GetTextFromUser  ( $_[0], $_[1], "", _parent()) }
+sub get_number {Wx::GetNumberFromUser( $_[0], '', $_[1],$_[2], 0, 100000, _parent())}
+
+# own dialogs
+#sub find {
+	#require Kephra::App::Dialog::Search; &Kephra::App::Dialog::Search::find;
+#}
+#sub replace {
+	#require Kephra::App::Dialog::Search; &Kephra::App::Dialog::Search::replace;
+#}
+#sub choose_color {
+	#require Kephra::App::Dialog::Color; Kephra::App::Dialog::Color::choose_color();
+#}
+sub about {
+	require Kephra::App::Dialog::About;
+	Kephra::App::Dialog::About::create( )->ShowModal;
+}
+sub config {
+	require Kephra::App::Dialog::Config;
+	Kephra::App::Dialog::Config::create( )->ShowModal;
+}
+sub documentation {
+	require Kephra::App::Dialog::Documentation;
+	Kephra::App::Dialog::Documentation::create( )->ShowModal;
+}
+sub keymap {
+	require Kephra::App::Dialog::Keymap;
+	Kephra::App::Dialog::Keymap::create( )->ShowModal;
+}
+
+#sub notify_file_changed {
+	#require Kephra::App::Dialog::Notify; &Kephra::App::Dialog::Notify::file_changed;
+#}
+#sub notify_file_deleted {
+	#require Kephra::App::Dialog::Notify; &Kephra::App::Dialog::Notify::file_deleted;
+#}
+#sub save_on_exit {
+	#require Kephra::App::Dialog::Exit; &Kephra::App::Dialog::Exit::save_on_exit;
+#}
+
+
+1;

lib/Kephra/App/Focus.pm

+use v5.12;
+use warnings;
+use Wx;
+
+package Kephra::App::Focus;
+my @focus_stack = ();
+my $max_stack_size = 50;
+
+use Kephra::App::Util qw(is_widget);
+
+#Kephra::API::Command::register({
+ #'app-focus-set-back'  =>{ sub=>'set_back', label=>'',  keys=>'' },
+ #'app-focus-stay'      =>{ sub=>'stay',     label=>'',  keys=>'' },
+#});
+
+
+sub get  {                              # give widget in focus or one that was before
+	my ($nr) = shift // 0;
+	return '' if @focus_stack == 0;
+	return $focus_stack[$nr] if $nr < @focus_stack or $nr < 0 and $nr > @focus_stack;
+	return '';
+}
+
+sub last {                              # give me last widget of a class in focus
+	my $class = shift;
+	return Kephra::Log::warning( "need a class name", 1 ) unless $class;
+	for (@focus_stack) { return $_ if ref $_ eq $class }
+}
+
+sub set  {                              # set focus to another widget
+	my ($widget) = @_;
+	return unless defined $widget;
+	#Kephra::Log::note("focus is on :". get()." and will be: $widget");
+	return Kephra::Log::warning( "got no proper widget but $widget", 1 ) unless is_widget($widget);
+	return if $widget eq get();
+	unshift @focus_stack, $widget;
+	pop @focus_stack while stack_size() > $max_stack_size;
+	stay();
+}
+
+sub set_back {                          # undo n times (default 1) a set command
+	my ($times) = shift // 1;
+	for (1 .. $times) { shift @focus_stack if stack_size() > 1 }
+	stay();
+}
+
+sub stay {                              # reset focus to current (last set) entry
+	my ($widget) = get();
+	is_widget($widget)
+		? Wx::Window::SetFocus( $widget )
+		: Kephra::Log::warning( "no widget to stay focussed on", 1 );
+}
+
+sub stack_size { scalar @focus_stack }  # get current size (number  of memorized widgets)
+
+sub max_stack_size {                    # get and set maximum number of memorized widgets in focus
+	my $new_stack_size = shift;
+	$max_stack_size = defined $new_stack_size ? $new_stack_size : $max_stack_size;
+}
+
+
+1;

lib/Kephra/App/Util.pm

+use strict;
+use warnings;
+use Exporter;
+use Wx;
+
+package Kephra::App::Util;
+our @ISA       = qw(Exporter);
+our @EXPORT_OK = qw(is_widget is_panel is_sizer is_color is_font create_color lounch_browser);
+use Scalar::Util qw(blessed looks_like_number);
+
+
+# export a helper function into a coderef
+sub get {
+	my ($ref, @method_ref);
+	die unless @_;
+	for my $subname (@_){
+		eval { $ref = \&{__PACKAGE__ . "::$subname"} };
+		if ($@) { Kephra::Log::error( "no $subname sub in this Module.") }
+		else    { push @method_ref, $ref }
+	}
+	if (wantarray){ return @method_ref }
+	else          { return $method_ref[0] }
+}
+
+sub is_widget {(blessed($_[0]) and $_[0]->isa('Wx::Window')              )  ? 1 : 0}
+sub is_panel  {(blessed($_[0]) and $_[0]->isa('Wx::Panel')               )  ? 1 : 0}
+sub is_sizer  {(blessed($_[0]) and $_[0]->isa('Wx::Sizer')               )  ? 1 : 0}
+sub is_color  {(blessed($_[0]) and $_[0]->isa('Wx::Colour') and $_[0]->IsOk)? 1 : 0}
+sub is_font   {(blessed($_[0]) and $_[0]->isa('Wx::Font') and $_[0]->IsOk)  ? 1 : 0}
+
+sub create_color {
+	my ($r, $g, $b, $t ) = @_;
+	my $name = __PACKAGE__ . '::make_color';
+	my $usage = "$name takes 1 parameter (hex-'12345f' or dec-'0,0,1' or name) or 3 parameters (R, G, B)";
+	die "not enough parmaters\n$usage" unless defined $r;
+	unless (defined $g){
+		# handle hex format like in HTML: #234567
+		if ( $r =~ /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i ) {
+			($r,$g,$b) = ( hex $1, hex $2, hex $3 );
+		}
+		# handle comma-seperated format: 0,0,125
+		elsif ( $r =~ /^(\d+),\s*(\d+),\s*(\d+)$/ ) {
+			($r,$g,$b) = ( $1+0, $2+0, $3+0 );
+		} else {
+			my %color = (
+				'white' => [255, 255, 255],
+				'red'   => [255,   0,   0],
+				'orange'=> [255, 127,   0],
+				'yellow'=> [255, 255,   0],
+				'green' => [  0, 255,   0],
+				'cyan'  => [  0, 255, 255],
+				'blue'  => [  0,   0, 255],
+				'purple'=> [255,   0, 255],
+				'violet'=> [255,   0, 255],
+				'grey'  => [180, 180, 180],
+				'black' => [  0,   0,   0],
+			);
+			if ( exists $color{lc $r} ) {  ( $r, $g, $b ) = @{ $color{lc $r} }  }
+			else {
+				my $c = Wx::ColourDatabase::Find( $r );
+				return $c if is_color( $c );
+			}
+			unless (defined $g) {
+				my @clist = ( (keys %color),
+					'AQUAMARINE', 'BLACK', 'BLUE', 'BLUE VIOLET', 'BROWN',
+					'CADET BLUE', 'CORAL', 'CORNFLOWER BLUE', 'CYAN', 'DARK GREY',
+					'DARK GREEN', 'DARK OLIVE GREEN', 'DARK ORCHID', 'DARK SLATE BLUE',
+					'DARK SLATE GREY', 'DARK TURQUOISE, DIM GREY, FIREBRICK',
+					'FOREST GREEN', 'GOLD', 'GOLDENROD', 'GREY, GREEN', 'GREEN YELLOW',
+					'INDIAN RED', 'KHAKI', 'LIGHT BLUE', 'LIGHT GREY', 'LIGHT STEEL BLUE',
+					'LIME GREEN', 'MAGENTA', 'MAROON', 'MEDIUM AQUAMARINE', 'MEDIUM BLUE',
+					'MEDIUM FOREST GREEN', 'MEDIUM GOLDENROD', 'MEDIUM ORCHID', 
+					'MEDIUM SEA GREEN', 'MEDIUM SLATE BLUE', 'MEDIUM SPRING GREEN',
+					'MEDIUM TURQUOISE', 'MEDIUM VIOLET RED', 'MIDNIGHT BLUE, NAVY',
+					'ORANGE', 'ORANGE RED', 'ORCHID', 'PALE GREEN', 'PINK', 'PLUM',
+					'PURPLE', 'RED, SALMON', 'SEA GREEN', 'SIENNA', 'SKY BLUE',
+					'SLATE BLUE', 'SPRING GREEN', 'STEEL BLUE', 'TAN', 'THISTLE',
+					'TURQUOISE', 'VIOLET', 'VIOLET RED', 'WHEAT', 'WHITE', 'YELLOW',
+					'YELLOW GREEN',
+				);
+				die "$r is an unknow color name, just know @clist";
+			}
+		}
+	}
+	die "not enough parmaters or bad format\n$usage" unless defined $b;
+	die "too much parmaters\n$usage" if defined $t;
+	die "red definition is outside of 0..255" unless int $r == $r && $r>=0 && $r<=255;
+	die "green definition is outside of 0..255" unless int $g == $g && $g>=0 && $g<=255;
+	die "blue definition is outside of 0..255" unless int $b == $b && $b>=0 && $b<=255;
+	return Wx::Colour->new($r, $g, $b);
+}
+
+sub lounch_browser { Wx::LaunchDefaultBrowser($_[0]) }
+
+
+1;
+
+__END__
+
+	#my $normal_font = Wx::Font->new (
+		#12, 
+		#&Wx::wxFONTFAMILY_DEFAULT,
+		#&Wx::wxFONTSTYLE_NORMAL,
+		#&Wx::wxFONTWEIGHT_NORMAL,
+		#0,
+	#);
+	#my $highlight_font = Wx::Font->new (
+		#12, 
+		#&Wx::wxFONTFAMILY_TELETYPE,
+		#&Wx::wxFONTSTYLE_NORMAL,
+		#&Wx::wxFONTWEIGHT_BOLD,
+		#1,
+	#);

lib/Kephra/App/Window.pm

 	my $ed = Kephra::App::Editor->new($self, -1);
 
 	Wx::Window::SetFocus( $ed );
-	#Kephra::App::Focus::set($ed);
+	Kephra::API::focus($ed);
 
 	return $self;
 }
 
+
 sub close   { $_ref->Close() }
 sub destroy { $_ref->Destroy() }
 

lib/Kephra/Document.pm

+use v5.10;
+use File::Spec;
+use Kephra::API;
+use Kephra::File;
+
+package Kephra::Document;
+use Moo;
+use Scalar::Util qw(blessed);
+
+my $max_title_width = 15;                               # export SUB_QUOTE_DEBUG=1
+
+has ID       => ( is  => 'rwp' );                             # API::Doc ID
+has title    => ( is  => 'rwp' );                             # shown as tab label
+has editor   => ( is  => 'rwp', default => sub { {} } );
+has panel    => ( is  => 'rwp', default => sub { {} } );
+has anon_NR  => ( is  => 'rw' );
+has file_path=> ( 
+	is       => 'rw',
+	coerce   =>  sub { Kephra::File::normalize_path($_[0]) },
+	trigger  =>  sub {
+		my $self = shift;
+		return if $self->prev_file_path and $self->file_path
+				and $self->prev_file_path eq $self->file_path;
+		Kephra::API::Doc::rename_file($self) if $self->ID;
+
+		if ($self->file_path) {
+			my @path_parts = File::Spec->splitpath( $self->file_path );
+			$self->_set_file_dir( $path_parts[1] );
+			$self->_set_file_name( $path_parts[2] );
+
+			my $title = $path_parts[2];
+			$title = substr( $title, 0, $max_title_width - 2 ) . '..'
+				if length($title) > $max_title_width and $max_title_width > 7;
+			$self->_set_title($title);
+		} else {
+			$self->_set_title('<untitled '.$self->anon_NR.'>');
+		}
+
+		$self->_set_prev_file_path( $self->file_path );
+		$_->set_page_title( $self->title, $self->panel->{$_} ) for Kephra::API::all_docbars();
+	});
+has file_dir      => ( is => 'rwp', );
+has file_name     => ( is => 'rwp', );
+has prev_file_path=> ( is => 'rwp' );
+has content       => ( is => 'rw', reader=> sub{ $_[0] } );
+has notes         => ( is => 'rw', );
+
+has syntax_mode   => ( is => 'rw', default => sub { 'perl' } );
+has readonly      => ( is => 'rw', );
+has active_config => ( is => 'rw', );
+has caret_pos     => ( is => 'rw', );
+has edit_pos      => ( is => 'rw', );
+has encoding      => ( is => 'rw', );
+has EOL           => ( is => 'rw', );
+has tab_size      => ( is => 'rw', );
+has tab_use       => ( is => 'rw', );
+has folded_lines  => ( is => 'rw', );
+has marked_lines  => ( is => 'rw', );
+
+
+sub BUILDARGS {
+	my ( $self, @args ) = @_;
+	#unshift @args, "file_path" if @args % 2 == 1;
+	my %args;
+	%args = @args unless scalar @args % 2;
+	$args{'encoding'} = 'utf-8';
+
+	return \%args;
+}
+sub BUILD    {
+	my $self = shift;
+	$self->add_instance($_) for Kephra::API::all_docbars();
+	$self->_set_ID( Kephra::API::Doc::add($self) );
+	$self->_set_title('<untitled '.$self->anon_NR.'>') unless $self->file_path;
+}
+sub DEMOLISH { Kephra::API::Doc::remove($_[0]) }
+
+
+sub add_instance {
+	my ($self, $bar) = @_;
+	return Kephra::Log::warning("need an existing Kephra::App::Bar::Document instance")
+		unless blessed($bar) eq 'Kephra::App::Bar::Document';
+
+	my @editor = values $self->editor;
+	my $panel = $self->panel->{$bar} = Kephra::App::Panel->new( $bar );
+	my $ed = $self->editor->{$bar} = Kephra::App::Editor->new( $panel );
+	$panel->append( \$ed );
+	$ed->SetDocPointer( $editor[0]->GetDocPointer() ) if @editor;
+	Kephra::API::Doc::add_instance($self, $bar);
+}
+
+sub del_instance {
+	my ($self, $bar) = @_;
+	return Kephra::Log::warning("need an existing Kephra::App::Bar::Document instance")
+		unless blessed($bar) eq 'Kephra::App::Bar::Document'
+		and exists $self->panel->{$bar};
+	Kephra::API::Doc::remove_instance($self, $bar);
+	$bar->remove_page( $self->panel->{$bar} );
+	delete $self->panel->{$bar};
+	delete $self->editor->{$bar};
+}
+sub raise { Kephra::API::focus( shift->panel ) }
+sub insert_before_caret {}
+sub insert_after_caret {}
+
+1;

lib/Kephra/Edit.pm

+use strict;
+use warnings;
+
+use Wx;
+use Kephra::API;
+use Kephra::Config;
+
+#package Kephra::Edit;
+
+package Kephra::App::Editor;
+
+Kephra::API::Command::register({
+ 'edit-undo'             =>{sub=>'$ed->Undo',             label=>'Undo',          keys=> 'ctrl+z'},
+ 'edit-redo'             =>{sub=>'$ed->Redo',             label=>'Redo',          keys=> 'ctrl+shift+z'},
+ 'edit-copy'             =>{sub=>'$ed->Copy',             label=>'Copy',          keys=> 'ctrl+c' },
+ 'edit-copy-line'        =>{sub=>'$ed->LineCopy',         label=>'Copy Line',     keys=> 'ctrl+shift+c'},
+ 'edit-cut'              =>{sub=>'$ed->Cut',              label=>'Cut',           keys=> 'ctrl+x'},
+ 'edit-delete'           =>{sub=>'$ed->Clear',            label=>'Delete',        keys=> 'delete'},
+ 'edit-delete-back'      =>{sub=>'$ed->DeleteBack',       label=>'Del Back',      keys=> 'back'},
+ 'edit-delete-word-left' =>{sub=>'$ed->DelWordLeft',      label=>'Del Word Left', keys=> 'ctrl+back'},
+ 'edit-delete-word-right'=>{sub=>'$ed->DelWordRight',     label=>'Del Word Right',keys=> 'ctrl+delete'},
+ 'edit-delete-line'      =>{sub=>'$ed->LineDelete',       label=>'Del Line',      keys=> 'ctrl+shift+delete'},
+ 'edit-insert'           =>{sub=>'$ed->Paste',            label=>'Insert',        keys=> 'ctrl+v'}, # thats what sometimes called paste
+ 'edit-insert-newline'   =>{sub=>'$ed->NewLine',          label=>'Newline',       keys=> 'enter'},
+ 'edit-replace'          =>{sub=>'$ed->replace',          label=>'Replace',       keys=> 'ctrl+shift+v'},
+ 'edit-double'           =>{sub=>'$ed->SelectionDuplicate',label=>'Double',       keys=> 'ctrl+d'},
+ 'edit-select-form'      =>{sub=>'$ed->select_form',      label=>'Form',          keys=> 'ctrl+y'},
+ 'edit-select-content'   =>{sub=>'$ed->select_content',   label=>'Content',       keys=> 'ctrl+shift+y'},
+});
+
+sub get_clipboard_text {
+	my $text;
+	my $cboard = &Wx::wxTheClipboard;
+	$cboard->Open;
+	if ( $cboard->IsSupported( &Wx::wxDF_TEXT ) ) {
+		my $data = Wx::TextDataObject->new;
+		my $ok = $cboard->GetData( $data );
+		if ( $ok ) { $text = $data->GetText; }
+		else { # todo: error handling
+		}
+	}
+	$cboard->Close;
+	$text = '' unless defined $text and $text;
+}
+
+#sub visual_height {	my ($self) = @_; 	$self->GetFirstVisibleLine; $self->}
+
+sub replace {
+	my ($self) = @_;
+	return unless $self->CanPaste;
+	my ($begin, $end_a) = $self->GetSelection();
+	$self->BeginUndoAction;
+	$self->SetSelection($begin, $begin);
+	$self->Paste();
+	my $end_b = $self->GetCurrentPos();
+	$self->SetSelection($end_b, $end_b + $end_a - $begin);
+	$self->Cut();
+	$self->SetSelection($begin, $begin + $end_b - $begin);
+	$self->EndUndoAction;
+}
+
+sub select_form {
+	my ($self) = @_;
+
+}
+
+sub select_content {
+	my ($self) = @_;
+
+}
+
+1;

lib/Kephra/File.pm

+use v5.10;
+use strict;
+use warnings;
+use File::Spec;
+use Kephra::API;
+use Kephra::App::Dialog;
+use Kephra::File::Local;
+
+package Kephra::File;
+
+Kephra::API::Command::register({
+ 'file-new'        =>{sub=> 'new',          label=> 'New',        keys=> 'ctrl+n'},
+ 'file-open'       =>{sub=> 'open',         label=> 'Open ...',   keys=> 'ctrl+o'},
+ 'file-reopen'     =>{sub=> 'reopen_active',label=> 'Reopen',     keys=> 'ctrl+shift+o'},
+ 'file-save'       =>{sub=> 'save_active',  label=> 'Save',       keys=> 'ctrl+s'},
+ 'file-save-as'    =>{sub=> 'save_as',      label=> 'Save As ...',keys=> 'ctrl+shift+s'},
+ 'file-rename'     =>{sub=> 'rename',       label=> 'Rename ...', keys=> 'ctrl+alt+shift+s'},
+ 'file-close'      =>{sub=> 'close_active', label=> 'Close',      keys=> 'ctrl+q'},
+ 'file-close-other'=>{sub=> 'close_other',  label=> 'Close Other',keys=> 'ctrl+shift+q'},
+ 'file-close-all'  =>{sub=> 'close_all',    label=> 'Close All',  keys=> 'ctrl+alt+q'},
+});
+use Scalar::Util qw(blessed);
+sub normalize_path {
+	my $file = shift;
+	return unless defined $file and $file;
+
+	$file = File::Spec->canonpath($file);
+	local $/ = "\r\n";
+	chomp($file);
+	return $file;
+}
+
+
+sub new  {
+	my $docbar = Kephra::API::docbar();
+	my $doc = Kephra::Document->new();
+	$_->add_page( $doc->panel->{$_}, 'rightmost', $doc->title, 0 ) for Kephra::API::all_docbars();
+	Kephra::API::focus( $doc->editor->{$docbar} );
+	Kephra::API::main_window()->refresh_title();
+}
+
+sub open {
+	# open dialog if no file was given
+	@_ = Kephra::App::Dialog::get_files_open() unless @_;
+
+	# new doc for each file name
+	for my $file (@_){
+		$file = normalize_path( $file );
+		next unless $file and -e $file;
+
+		# raise tab if doc is alread open
+		Kephra::App::Part::Editor::raise_document($file), next
+		  if Kephra::API::Doc::file_loaded($file);
+
+		my $db = Kephra::API::docbar();
+		my $doc = Kephra::API::document();
+		my $ed = Kephra::API::editor();
+		if ($ed->GetLength) {
+			$doc = Kephra::Document->new();
+			$doc->editor->{$_}->unmount_events() for Kephra::API::all_docbars();
+			$_->add_page( $doc->panel->{$_},'right', $doc->file_name, 1) for Kephra::API::all_docbars();
+			my $content_ref = $doc->editor->{$db}->GetDocPointer();
+			$doc->file_path($file);
+			$doc->editor->{$_}->SetDocPointer( $content_ref ) for Kephra::API::passive_docbars();
+		}
+		# reuse it if current doc is empty
+		else {
+			$doc->editor->{$_}->unmount_events() for Kephra::API::all_docbars();
+			$doc->file_path($file) 
+		}
+		Kephra::API::app()->Yield;
+		Kephra::File::Local::read( $doc, $file );
+		$ed = Kephra::API::editor();
+		$ed->EmptyUndoBuffer;
+		$doc->editor->{$_}->mount_events() for Kephra::API::all_docbars();
+		$_->mount_events() for Kephra::API::all_docbars();
+		Kephra::API::main_window()->refresh_title();
+	}
+}
+
+sub reopen {
+	for my $doc (@_){
+		next unless ref $doc eq 'Kephra::Document';
+		Kephra::Log::warning("can't reopen nonexising file"), next
+			unless $doc->file_path and -e $doc->file_path;
+		Kephra::File::Local::read( $doc );
+	}
+}
+sub reopen_active { reopen( Kephra::API::document() ) }
+sub reopen_all    { reopen( Kephra::API::all_documents()   ) }
+
+
+sub save {
+	$_[0] = Kephra::API::document() unless @_;
+	my $docbar = Kephra::API::docbar();
+
+	for my $doc (@_){
+		next unless ref $doc eq 'Kephra::Document';
+		next unless $doc->editor->{$docbar}->GetModify;
+		if ($doc->file_path){
+			Kephra::File::Local::write($doc);
+		} else {
+			$doc->raise;
+			save_as($doc);
+		}
+	}
+}
+sub save_active { save( Kephra::API::document() ) }
+sub save_all    { save( Kephra::API::all_documents() ) }
+sub save_as {
+	my $doc = shift || Kephra::API::document();
+	my $file = normalize_name( Kephra::App::Dialog::get_file_save() );
+	return unless $file;
+	_write_file($doc, $file);
+	$doc->assign_file_path($file);
+	Kephra::API::docbar()->set_page_title( $doc->file_name);
+	Kephra::API::main_window()->refresh_title();
+}
+sub rename {
+	my $doc = shift || Kephra::API::document();
+	my $old_file = $doc->file_name;
+	save_as( $doc );
+	unlink $old_file;
+}
+
+
+sub close {
+	$_[0] = Kephra::API::document() unless @_;
+	my $active_bar = Kephra::API::docbar();
+
+	for my $doc (@_) {
+		next unless ref $doc eq 'Kephra::Document';
+		my $ed = $doc->editor->{ $active_bar };
+		if ($ed->GetModify) {
+			my $save_answer = Kephra::App::Dialog::yes_no_cancel (
+				'close unsaved file', 'do you want to save it before?'
+			);
+			next if $save_answer == &Wx::wxCANCEL;
+			save($doc) if $save_answer == &Wx::wxYES;
+		}
+		if ($active_bar->GetPageCount > 1){
+			$_->remove_page( $doc->panel->{$_} ) for Kephra::API::all_docbars();
+			$doc->DESTROY;
+		} else {
+			next unless $ed->GetLength;
+			$ed->ClearAll();
+			$ed->SetSavePoint;
+			$ed->EmptyUndoBuffer;
+			$doc->file_path('');
+		}
+	}
+	Kephra::API::main_window()->refresh_title();
+	Kephra::API::focus($active_bar->GetSelection)
+		if blessed($active_bar) and $active_bar->GetSelection;
+}
+sub close_active { Kephra::File::close( Kephra::API::document() )}
+sub close_all    { Kephra::File::close( Kephra::API::all_documents() ) }
+sub close_other  {
+	my $doc = Kephra::API::document();
+	for (Kephra::API::all_documents()) { Kephra::File::close($_) unless $_ eq $doc }
+}
+
+
+1;