package GCE::MapEditor; =head1 NAME GCE::MapEditor - the map editing widget =cut use Gtk2; use Gtk2::Gdk::Keysyms; use Gtk2::SimpleMenu; use Crossfire; use Crossfire::Map; use Crossfire::MapWidget; use GCE::AttrEdit; use GCE::Util; use GCE::HashDialog; use POSIX qw/strftime/; use Glib::Object::Subclass Gtk2::Window; use Storable qw/dclone/; use strict; ################################################################# ###### WINDOW MANAGEMENT ######################################## ################################################################# sub save_layout { my ($self) = @_; $self->{attach_editor}->save_layout if $self->{attach_editor}; $self->{map_properties}->save_layout if $self->{map_properties}; $self->{meta_info_win}->save_layout if $self->{meta_info_win}; $main::CFG->{map_info} = main::get_pos_and_size ($self->{map_info}) if $self->{map_info}; } sub close_windows { my ($self) = @_; $self->{attach_editor}->destroy if $self->{attach_editor}; $self->{map_properties}->destroy if $self->{map_properties}; $self->{meta_info_win}->destroy if $self->{meta_info_win}; } ################################################################# ###### MENU MANAGEMENT ########################################## ################################################################# sub do_context_menu { my ($self, $map, $event) = @_; my ($x, $y) = $map->coord ($event->x, $event->y); my $menu = Gtk2::Menu->new; foreach my $cm ( [ Follow => sub { $::MAINWIN->{edit_collection}{followexit}->edit ($map, $x, $y, $self) }, ] ) { my $item = Gtk2::MenuItem->new ($cm->[0]); $menu->append ($item); $item->show; $item->signal_connect (activate => $cm->[1]); } $menu->append (my $sep = new Gtk2::SeparatorMenuItem); $sep->show; for my $sr (reverse $self->get_stack_refs ($map, $x, $y)) { my $item = Gtk2::MenuItem->new ($sr->longname); $menu->append ($item); $item->set_submenu (my $smenu = new Gtk2::Menu); for my $act ( [ 'Add inventory' => sub { $_[0]->add_inv ($::MAINWIN->get_pick) } ], [ 'Find in picker' => sub { $::MAINWIN->open_pick_window ({ selection => $sr->picker_folder }) } ], ) { my $sitem = Gtk2::MenuItem->new ($act->[0]); $smenu->append ($sitem); $sitem->signal_connect (activate => sub { $act->[1]->($sr) }); $sitem->show; } $item->show; } $menu->popup (undef, undef, undef, undef, $event->button, $event->time); } sub build_menu { my ($self) = @_; my $menu_tree = [ _File => { item_type => '', children => [ "_Save" => { callback => sub { $self->save_map }, accelerator => 'S' }, "Save As" => { callback => sub { $self->save_map_as }, }, "Map _Info" => { callback => sub { $self->open_map_info }, accelerator => "I", }, "Map _Properties" => { callback => sub { $self->open_map_prop }, accelerator => "P" }, "Map _Attachments" => { callback => sub { $self->open_attach_edit }, accelerator => "A" }, "Map Meta _Info" => { callback => sub { $self->open_meta_info }, }, Upload => { item_type => '', children => [ "Upload for testing" => { callback => sub { $self->upload_map_test }, }, "Upload for inclusion" => { callback => sub { $self->upload_map_incl }, }, ] }, "_Map Resize" => { callback => sub { $self->open_resize_map }, }, "Close" => { callback => sub { $self->destroy }, }, ] }, _Edit => { item_type => '', children => [ "_Undo" => { callback => sub { $self->undo }, accelerator => "Z" }, "_Redo" => { callback => sub { $self->redo }, accelerator => "Y" }, ] }, _Go => { item_type => '', children => [ "_Up" => { callback => sub { $self->follow ('u') }, accelerator => "Up" }, "_Down" => { callback => sub { $self->follow ('d') }, accelerator => "Down" }, "_Right" => { callback => sub { $self->follow ('r') }, accelerator => "Right" }, "_Left" => { callback => sub { $self->follow ('l') }, accelerator => "Left" }, ] }, _Help => { item_type => '', children => [ _Manual => { callback => sub { $::MAINWIN->show_help_window }, accelerator => "H" }, ] }, ]; my $men = Gtk2::SimpleMenu->new ( menu_tree => $menu_tree, default_callback => \&default_cb, ); for ( [i => 'pick'], [p => 'place'], [e => 'erase'], [s => 'select'], [l => 'eval'], [t => 'connect'], [f => 'followexit'] ) { my $tool = $_->[1]; $men->{accel_group}->connect ($Gtk2::Gdk::Keysyms{$_->[0]}, [], 'visible', sub { $::MAINWIN->set_edit_tool ($tool) }); } $men->{accel_group}->connect ($Gtk2::Gdk::Keysyms{'r'}, ['control-mask'], 'visible', sub { $self->redo }); $self->add_accel_group ($men->{accel_group}); return $men->{widget}; } ################################################################# ###### EDIT TOOL STUFF ########################################## ################################################################# sub set_edit_tool { my ($self, $tool) = @_; $self->{etool} = $tool; if ($self->ea->special_arrow) { $self->{map}{window}->set_cursor (Gtk2::Gdk::Cursor->new ($self->ea->special_arrow)); } else { # FIXME: Get the original cursor and insert it here $self->{map}{window}->set_cursor (Gtk2::Gdk::Cursor->new ('GDK_LEFT_PTR')); } } sub ea { my ($self) = @_; $self->{ea_alt} || $self->{etool}; } sub start_drawmode { my ($self, $map) = @_; $self->{draw_mode} and return; # XXX: is this okay? my ($x, $y) = $map->coord ($event->x, $event->y); my ($x, $y) = $map->coord ($map->get_pointer); my $ea = $self->ea; $ea->begin ($map, $x, $y, $self); $ea->edit ($map, $x, $y, $self) if $x >= 0 and $y >= 0 and $x < $map->{map}{width} and $y < $map->{map}{height}; $self->{draw_mode} = [$x, $y]; } sub stop_drawmode { my ($self, $map) = @_; $self->{draw_mode} or return; my ($x, $y) = $map->coord ($map->get_pointer); my $ea = $self->ea; $ea->end ($map, $x, $y, $self); delete $self->{draw_mode}; } ################################################################# ###### UTILITY FUNCTIONS ######################################## ################################################################# sub follow { my ($self, $dir) = @_; my %dir_to_path = ( u => 'tile_path_1', d => 'tile_path_3', r => 'tile_path_2', l => 'tile_path_4', ); defined $dir_to_path{$dir} or return; my $map = $self->{map}{map}{info}{$dir_to_path{$dir}} or return; $map = map2abs ($map, $self); $::MAINWIN->open_map_editor ($map); } # FIXME: Fix the automatic update of the attribute editor! and also the stack view! sub undo { my ($self) = @_; my $map = $self->{map}; # the Crossfire::MapWidget $map->{undo_stack_pos} or return; $map->change_swap ($map->{undo_stack}[--$map->{undo_stack_pos}]); } sub get_stack_refs { my ($self, $map, $x, $y) = @_; my $cstack = $map->get ($x, $y); return [] unless @$cstack; my @refs; for my $arch (@$cstack) { my ($ox, $oy) = ($x, $y); if ($arch->{_virtual}) { $ox = $arch->{virtual_x}; $oy = $arch->{virtual_y}; $arch = $arch->{_virtual}; $cstack = $map->get ($ox, $oy); # XXX: This heavily blows up if $arch isn't on $cstack now.. and it actually really does :( } push @refs, GCE::ArchRef->new ( arch => $arch, cb => sub { $map->change_begin ('attredit'); $map->change_stack ($ox, $oy, $cstack); if (my $changeset = $map->change_end) { splice @{ $map->{undo_stack} ||= [] }, $map->{undo_stack_pos}++, 1e6, $changeset; } } ); } return @refs; } sub redo { my ($self) = @_; my $map = $self->{map}; # the Crossfire::MapWidget $map->{undo_stack} and $map->{undo_stack_pos} < @{$map->{undo_stack}} or return; $map->change_swap ($map->{undo_stack}[$map->{undo_stack_pos}++]); } sub load_meta_info { my ($mapfile) = @_; if (-e "$mapfile.meta") { open my $metafh, "<", "$mapfile.meta" or warn "Couldn't open meta file $mapfile.meta: $!"; my $metadata = do { local $/; <$metafh> }; return Crossfire::from_json ($metadata); } } sub save_meta_info { my ($mapfile, $metainfo) = @_; open my $metafh, ">", "$mapfile.meta" or warn "Couldn't write meta file $mapfile.meta: $!"; print $metafh Crossfire::to_json ($metainfo); } sub open_map { my ($self, $path, $key) = @_; $self->{mapkey} = $key; if (ref $path) { $self->{map}->set_map ($path); delete $self->{meta_info}; $self->set_title (''); } else { my $ok = 0; if (-e $path && -f $path) { $ok = 1; } else { unless ($path =~ m/\.map$/) { # yuck my $p = $path . '.map'; if ($ok = -e $p && -f $p) { $path = $p; } } } unless ($ok) { die "Couldn't open '$path' or find '$path.map': No such file or it is not a file.\n"; } $self->{path} = $path; $self->{map}->set_map (my $m = new_from_file Crossfire::Map $path); $self->{meta_info} = load_meta_info ($path); $self->set_title ("gce - map editor - $self->{path}"); } $self->close_windows; } sub save_map { my ($self) = @_; if ($self->{path}) { $self->{map}{map}->write_file ($self->{path}); if ($self->{meta_info}) { save_meta_info ($self->{path}, $self->{meta_info}); } quick_msg ($self, "saved to $self->{path}"); $self->set_title ("gce - map editor - $self->{path}"); } else { $self->save_map_as; } } sub save_map_as { my ($self) = @_; my $fc = $::MAINWIN->new_filechooser ('gce - save map', 1, $self->{path}); if ('ok' eq $fc->run) { $::MAINWIN->{fc_last_folder} = $fc->get_current_folder; $::MAINWIN->{fc_last_folders}->{$self->{fc_last_folder}}++; $self->{map}{map}->write_file ($self->{path} = $fc->get_filename); if ($self->{meta_info}) { save_meta_info ($self->{path}, $self->{meta_info}); } quick_msg ($self, "saved to $self->{path}"); $self->set_title ("gce - map editor - $self->{path}"); } $fc->destroy; } ################################################################# ###### DIALOGOUES ############################################### ################################################################# sub open_resize_map { my ($self) = @_; return if $self->{meta_info_win}; my $w = $self->{meta_info_win} = GCE::HashDialogue->new (); $w->init ( dialog_default_size => [500, 200, 220, 20], layout_name => 'resize_win', title => 'resize map', ref_hash => $self->{map}{map}{info}, dialog => [ [width => 'Width' => 'string'], [height => 'Height' => 'string'], ], close_on_save => 1, save_cb => sub { my ($info) = @_; $self->{map}{map}->resize ($info->{width}, $info->{height}); $self->{map}->invalidate_all; } ); $w->signal_connect (destroy => sub { delete $self->{meta_info_win} }); $w->show_all; } sub open_attach_edit { my ($self) = @_; my $w = GCE::AttachEditor->new; $w->set_attachment ( $self->{map}{map}{info}{attach}, sub { if (@{$_[0]}) { $self->{map}{map}{info}{attach} = $_[0] } else { delete $self->{map}{map}{info}{attach}; } } ); $self->{attach_editor} = $w; $w->signal_connect (destroy => sub { delete $self->{attach_editor} }); $w->show_all; } sub upload_map_incl { my ($self) = @_; my $meta = dclone $self->{meta_info}; my $w = $self->{meta_info_win} = GCE::HashDialogue->new (); $w->init ( dialog_default_size => [500, 300, 220, 20], layout_name => 'map_upload_incl', title => 'gce - map inclusion upload', ref_hash => $meta, text_entry => { key => 'changes', label => 'Changes (required for inclusion):' }, dialog => [ [gameserver => 'Game server' => 'label'], [testserver => 'Test server' => 'label'], [undef => x => 'sep' ], [cf_login => 'Server login name' => 'string'], [cf_password=> 'Password' => 'password'], [path => 'Map path' => 'string'], ], close_on_save => 1, save_cb => sub { my ($meta) = @_; warn "UPLOAD[".Crossfire::to_json ($meta)."]\n"; } ); $w->signal_connect (destroy => sub { delete $self->{meta_info_win} }); $w->show_all; } sub upload_map_test { my ($self) = @_; my $meta = dclone $self->{meta_info}; my $w = $self->{meta_info_win} = GCE::HashDialogue->new (); $w->init ( dialog_default_size => [500, 300, 220, 20], layout_name => 'map_upload_test', title => 'gce - map test upload', ref_hash => $meta, dialog => [ [gameserver => 'Game server' => 'string'], [testserver => 'Test server' => 'string'], [undef => x => 'sep' ], [cf_login => 'Server login name' => 'string'], [cf_password=> 'Password' => 'password'], [path => 'Map path' => 'string'], ], save_cb => sub { my ($meta) = @_; warn "UPLOAD[".Crossfire::to_json ($meta)."]\n"; } ); $w->signal_connect (destroy => sub { delete $self->{meta_info_win} }); $w->show_all; } sub open_meta_info { my ($self) = @_; return if $self->{meta_info_win}; my $w = $self->{meta_info_win} = GCE::HashDialogue->new (); $w->init ( dialog_default_size => [500, 300, 220, 20], layout_name => 'meta_info_win', title => 'meta info', ref_hash => $self->{meta_info}, dialog => [ [path => 'Map path' => 'string'], [cf_login => 'Login name' => 'string'], [revision => 'CVS Revision' => 'label'], [cvs_root => 'CVS Root' => 'label'], [lib_root => 'LIB Root' => 'label'], [testserver => 'Test server' => 'label'], [gameserver => 'Game server' => 'label'], ], ); $w->signal_connect (destroy => sub { delete $self->{meta_info_win} }); $w->show_all; } sub open_map_info { my ($self) = @_; return if $self->{map_info}; my $w = $self->{map_info} = Gtk2::Window->new ('toplevel'); $w->set_title ("gcrossedit - map info"); $w->add (my $vb = Gtk2::VBox->new); $vb->add (my $sw = Gtk2::ScrolledWindow->new); $sw->set_policy ('automatic', 'automatic'); $sw->add (my $txt = Gtk2::TextView->new); $vb->pack_start (my $hb = Gtk2::HBox->new (1, 1), 0, 1, 0); $hb->pack_start (my $svbtn = Gtk2::Button->new ("save"), 1, 1, 0); $hb->pack_start (my $logbtn = Gtk2::Button->new ("add log"), 1, 1, 0); $hb->pack_start (my $closebtn = Gtk2::Button->new ("close"), 1, 1, 0); my $buf = $txt->get_buffer (); $buf->set_text ($self->{map}{map}{info}{msg}); $svbtn->signal_connect (clicked => sub { my $buf = $txt->get_buffer (); my $txt = $buf->get_text ($buf->get_start_iter, $buf->get_end_iter, 0); $self->{map}{map}{info}{msg} = $txt; }); $logbtn->signal_connect (clicked => sub { my $buf = $txt->get_buffer (); $buf->insert ($buf->get_start_iter, "- " . strftime ("%F %T %Z", localtime (time)) . " by " . ($main::CFG->{username} || $ENV{USER}) . ":\n"); $txt->set_buffer ($buf); }); $closebtn->signal_connect (clicked => sub { $w->destroy; }); ::set_pos_and_size ($w, $main::CFG->{map_info}, 400, 400, 220, 20); $w->signal_connect (destroy => sub { delete $self->{map_info}; }); $w->show_all; } sub open_map_prop { my ($self) = @_; return if $self->{map_properties}; my $w = $self->{map_properties} = GCE::HashDialogue->new (); $w->init ( dialog_default_size => [500, 500, 220, 20], layout_name => 'map_prop_win', title => 'map properties', ref_hash => $self->{map}{map}{info}, close_on_save => 1, dialog => [ [qw/name Name string/], [qw/region Region string/], [qw/enter_x Enter-x string/], [qw/enter_y Enter-y string/], [qw/reset_timeout Reset-timeout string/], [qw/swap_time Swap-timeout string/], [undef, qw/x sep/], [qw/difficulty Difficulty string/], [qw/windspeed Windspeed string/], [qw/pressure Pressure string/], [qw/humid Humid string/], [qw/temp Temp string/], [qw/darkness Darkness string/], [qw/sky Sky string/], [qw/winddir Winddir string/], [undef, qw/x sep/], [qw/width Width label/], # sub { $self->{map}{map}->resize ($_[0], $self->{map}{map}{height}) }], [qw/height Height label/], # sub { $self->{map}{map}->resize ($self->{map}{map}{width}, $_[0]) }], [undef, qw/x sep/], # [qw/msg Text text/], # [qw/maplore Maplore text/], [qw/outdoor Outdoor check/], [qw/unique Unique check/], [qw/fixed_resettime Fixed-resettime check/], [qw/per_player Per-Player check/], [undef, qw/x sep/], [qw/tile_path_1 Northpath string/], [qw/tile_path_2 Eastpath string/], [qw/tile_path_3 Southpath string/], [qw/tile_path_4 Westpath string/], [qw/tile_path_5 Toppath string/], [qw/tile_path_6 Bottompath string/], [undef, qw/x sep/], [undef, 'For shop description look in the manual', 'button', sub { $::MAINWIN->show_help_window }], [qw/shopmin Shopmin string/], [qw/shopmax Shopmax string/], [qw/shoprace Shoprace string/], [qw/shopgreed Shopgreed string/], [qw/shopitems Shopitems string/], ] ); $w->signal_connect (destroy => sub { delete $self->{map_properties} }); $w->show_all; } ################################################################# ###### MAP EDITOR INIT ########################################## ################################################################# sub INIT_INSTANCE { my ($self) = @_; $self->set_title ('gce - map editor'); $self->add (my $vb = Gtk2::VBox->new); $vb->pack_start (my $menu = $self->build_menu, 0, 1, 0); $vb->pack_start (my $map = $self->{map} = Crossfire::MapWidget->new, 1, 1, 0); $map->signal_connect_after (key_press_event => sub { my ($map, $event) = @_; my $kv = $event->keyval; my $ret = 0; my ($x, $y) = $map->coord ($map->get_pointer); for ([Control_L => sub { $self->{ea_alt} = $::MAINWIN->{edit_collection}{erase} }], [Alt_L => sub { $self->{ea_alt} = $::MAINWIN->{edit_collection}{pick} }], [c => sub { $::MAINWIN->{edit_collection}{select}->copy }], [v => sub { $::MAINWIN->{edit_collection}{select}->paste ($map, $x, $y) }], [n => sub { $::MAINWIN->{edit_collection}{select}->invoke }], ) { my $ed = $_; if ($kv == $Gtk2::Gdk::Keysyms{$ed->[0]}) { my $was_in_draw = defined $self->{draw_mode}; $self->stop_drawmode ($map) if $was_in_draw && grep { $ed->[0] eq $_ } qw/Control_L Alt_L/; $ed->[1]->(); $ret = 1; $self->start_drawmode ($map) if $was_in_draw && grep { $ed->[0] eq $_ } qw/Control_L Alt_L/; } } if ($self->ea->special_arrow) { $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ($self->ea->special_arrow)); } else { # FIXME: Get the original cursor and insert it here $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ('GDK_LEFT_PTR')); } $ret }); $map->signal_connect_after (key_release_event => sub { my ($map, $event) = @_; my $ret = 0; if ($event->keyval == $Gtk2::Gdk::Keysyms{Control_L} or $event->keyval == $Gtk2::Gdk::Keysyms{Alt_L}) { my $was_in_draw = defined $self->{draw_mode}; $self->stop_drawmode ($map) if $was_in_draw; delete $self->{ea_alt}; $ret = 1; $self->start_drawmode ($map) if $was_in_draw; } if ($self->ea->special_arrow) { $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ($self->ea->special_arrow)); } else { # FIXME: Get the original cursor and insert it here $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ('GDK_LEFT_PTR')); } $ret }); $map->signal_connect_after (button_press_event => sub { my ($map, $event) = @_; if ((not $self->{draw_mode}) and $event->button == 1) { my $ea = $self->ea; $self->start_drawmode ($map); $ea->want_cursor or $map->disable_tooltip; return 1; } elsif ($event->button == 3) { $self->do_context_menu ($map, $event); return 1; } 0 }); $map->signal_connect_after (motion_notify_event => sub { my ($map, $event) = @_; $self->{draw_mode} or return; my $ea = $self->ea; my ($X, $Y) = @{$self->{draw_mode}}[0,1]; my ($x, $y) = $map->coord ($map->get_pointer); while ($x != $X || $y != $Y) { $X++ if $X < $x; $X-- if $X > $x; $Y++ if $Y < $y; $Y-- if $Y > $y; unless ($ea->only_on_click) { $ea->edit ($map, $X, $Y, $self) if $X >= 0 and $Y >= 0 and $X < $map->{map}{width} and $Y < $map->{map}{height}; } } @{$self->{draw_mode}}[0,1] = ($X, $Y); 1 }); $map->signal_connect_after (button_release_event => sub { my ($map, $event) = @_; if ($self->{draw_mode} and $event->button == 1) { my $ea = $self->ea; $self->stop_drawmode ($map); $ea->want_cursor or $map->enable_tooltip; return 1; } 0 }); ::set_pos_and_size ($self, $main::CFG->{map_window}, 500, 500, 200, 0); } =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ Robin Redeker http://www.ta-sa.org/ =cut 1