Config-Model-TkUI

 view release on metacpan or  search on metacpan

lib/Config/Model/TkUI.pm  view on Meta::CPAN

#
# This file is part of Config-Model-TkUI
#
# This software is Copyright (c) 2008-2021 by Dominique Dumont.
#
# This is free software, licensed under:
#
#   The GNU Lesser General Public License, Version 2.1, February 1999
#
package Config::Model::TkUI 1.381;

use 5.20.1;
use strict;
use warnings;
use Carp;

use feature qw/postderef signatures/;
no warnings qw/experimental::postderef experimental::signatures/;

use base qw/Tk::Toplevel/;
use vars qw/$icon_path $error_img $warn_img/;
use subs qw/menu_struct/;
use Scalar::Util qw/weaken/;
use Log::Log4perl 1.11;
use Path::Tiny;
use YAML::PP;
use File::HomeDir;

use Pod::POM;
use Pod::POM::View::Text;

use Tk::DoubleClick;

use Tk::Balloon;
use Tk::Photo;
use Tk::PNG;    # required for Tk::Photo to be able to load pngs
use Tk::Adjuster;
use Tk::FontDialog;

use Tk::Pod;
use Tk::Pod::Text;    # for findpod

use Config::Model 2.135; # reset config clears changes

use Config::Model::Tk::Filter qw/apply_filter/;

use Config::Model::Tk::LeafEditor;
use Config::Model::Tk::CheckListEditor;

use Config::Model::Tk::LeafViewer;
use Config::Model::Tk::CheckListViewer;

use Config::Model::Tk::ListViewer;
use Config::Model::Tk::ListEditor;

use Config::Model::Tk::HashViewer;
use Config::Model::Tk::HashEditor;

use Config::Model::Tk::NodeViewer;
use Config::Model::Tk::NodeEditor;

use Config::Model::Tk::Wizard;
use Config::Model::Tk::CmeDialog;

Construct Tk::Widget 'ConfigModelUI';

my $cust_img;
my $tool_img;
my %gnome_img;

my $mod_file = 'Config/Model/TkUI.pm';
$icon_path = $INC{$mod_file};
$icon_path =~ s/TkUI.pm//;
$icon_path .= 'Tk/icons/';

my $logger = Log::Log4perl::get_logger('TkUI');

no warnings "redefine";

sub Tk::Error {
    my ( $cw, $error, @locations ) = @_;
    my $msg = ( ref($error) && $error->can('as_string') ) ? $error->as_string : $error;
    warn $msg;

lib/Config/Model/TkUI.pm  view on Meta::CPAN

    my $extra_menu = delete $args->{'-extra-menu'} || [];

    my $title = delete $args->{'-title'}
        || $0 . " " . $cw->{instance}->config_root->config_class_name;

    # check unknown parameters
    croak "Unknown parameter ", join( ' ', keys %$args ) if %$args;

    # initialize internal attributes
    $cw->{location} = '';
    $cw->{current_mode} = 'view';

    $cw->setup_scanner();

    # create top menu
    require Tk::Menubutton;
    my $menubar = $cw->Menu;
    $cw->configure( -menu => $menubar );
    $cw->{my_menu} = $menubar;

    $cw->add_file_menu($menubar, $extra_menu);
    $cw->add_help_menu($menubar);

    $cw->bind( '<Control-s>', sub { $cw->save } );
    $cw->bind( '<Control-q>', sub { $cw->quit } );
    $cw->bind( '<Control-c>', sub { $cw->edit_copy } );
    $cw->bind( '<Control-v>', sub { $cw->edit_paste } );
    $cw->bind( '<Control-f>', sub { $cw->pack_find_widget } );

    $cw->add_edit_menu($menubar);

    my $history_menu = $menubar->cascade(-label => 'History');

    my $option_menu = $menubar->cascade( -label => 'Options');
    $option_menu->command( -label => 'Font', -command => sub { $cw->set_font(); });

    # create 'hide empty values'
    $cw->{hide_empty_values} = 0;
    $option_menu->checkbutton(
        -label  => "Hide empty values",
        -variable => \$cw->{hide_empty_values},
        -command  => sub { $cw->reload($cw->{location}) },
    );

    # create 'show only custom values'
    $cw->{show_only_custom} = 0;
    $option_menu->checkbutton(
        -label => 'Show only custom values',
        -variable => \$cw->{show_only_custom},
        -command  => sub { $cw->reload($cw->{location}) },
    );

    # create 'show only custom values'
    $cw->{auto_save_mode} = 0;
    $option_menu->checkbutton(
        -label => 'Auto save',
        -variable => \$cw->{auto_save_mode},
    );

    my $weak_cw = $cw;
    weaken($weak_cw);
    $cw->{instance}->on_change_cb( sub {
        $weak_cw->save if $weak_cw->{auto_save_mode};;
    });

    # create frame for location entry
    my $loc_frame = $cw->Frame( -relief => 'sunken', -borderwidth => 1 )
        ->pack( -pady => 0, -fill => 'x' );
    $cw->{path_history} = [];
    $cw->{path_index} = 0;

    # add button
    my $previous_btn = $loc_frame->Button (
        -image => $gnome_img{'previous'},
        -state => 'disabled',
        -command => sub { $cw->go_to_previous();},
    );
    $previous_btn->pack(-side => 'left');
    my $next_btn = $loc_frame->Button (
        -image => $gnome_img{'next'},
        -state => 'disabled',
        -command => sub { $cw->go_to_next();},
    );
    $next_btn->pack(-side => 'left');

    $loc_frame->Label( -text => 'location :' )->pack( -side => 'left' );
    $loc_frame->Label( -textvariable => \$cw->{location} )->pack( -side => 'left' );

    # add bottom frame
    my $bottom_frame = $cw->Frame->pack(qw/-pady 0 -fill both -expand 1/);
    my $tree_frame = $bottom_frame->Frame->pack(qw/-fill both -expand 1 -side left/);
    my $filter_frame = $tree_frame->Frame->pack(qw/-fill x -side top/);

    # create the widget for tree navigation
    require Tk::Tree;
    my $tree = $tree_frame->Scrolled(
        qw/Tree/,
        -columns   => 4,
        -header    => 1,
        -opencmd   => sub { $cw->open_item(@_); },
        -closecmd  => sub { $cw->close_item(@_); },
    )->pack(qw/-fill both -expand 1 -side bottom/);

    $cw->{tktree} = $tree;

    my $sub_filter = sub {
        $cw->reload;
    };
    my $clear_filter = sub {
        $cw->{elt_filter_value} = '';
        $cw->reload;
    };
    my $reload_on_key = sub {
        $cw->reload;
    };

    my $filter_clear = $filter_frame->Button (
        -image => $gnome_img{'window-close'},
        -command => $clear_filter
    );
    $cw->Balloon(-state => 'balloon')->attach($filter_clear, -msg => 'clear filter');

lib/Config/Model/TkUI.pm  view on Meta::CPAN


sub prune {
    my $cw   = shift;
    my $path = shift;
    $logger->trace("prune $path");
    my %list = map { "$path." . to_path($_) => 1 } @_;

    # remove entries that are not part of the list
    my $tkt = $cw->{tktree};

    map { $tkt->deleteEntry($_) if $_ and not defined $list{$_}; } $tkt->infoChildren($path);
    $logger->trace("prune $path done");
}

# Beware: TkTree items store tree object and not tree cds path. These
# object might become irrelevant when warp master values are
# modified. So the whole Tk Tree layout must be redone every time a
# config value is modified. This is a bit heavy, but a smarter
# alternative would need hooks in the configuration tree to
# synchronise the Tk Tree with the configuration tree :-p

my %elt_mode = (
    leaf        => 'none',
    hash        => 'open',
    list        => 'open',
    node        => 'open',
    check_list  => 'none',
    warped_node => 'open',
);

sub disp_obj_elt {
    my ( $scanner, $data_ref, $node,    @orig_element_list ) = @_;
    my ( $path,    $cw,       $opening, $actions, $force_display_path ) = @$data_ref;
    my $tkt  = $cw->{tktree};
    my $mode = $tkt->getmode($path);

    my @element_list;
    foreach my $elt (@orig_element_list) {
        my $obj = $node->fetch_element($elt);
        my $loc = $obj->location;
        my $action = $actions->{$loc} // '';
        if ($action ne 'hide') {
            push @element_list, $elt;
        }
    }

    $logger->trace( "disp_obj_elt path $path mode $mode opening $opening " . "(@element_list)" );

    $cw->prune( $path, @element_list );

    my $node_loc = $node->location;

    my $prevpath = '';
    foreach my $elt (@element_list) {
        my $newpath  = "$path." . to_path($elt);
        my $scan_sub = sub {
            $scanner->scan_element( [ $newpath, $cw,  $opening, $actions, $force_display_path ], $node, $elt );
        };
        my @data = ( $scan_sub, $node->fetch_element($elt) );

        # It's necessary to store a weakened reference of a tree
        # object as these ones tend to disappear when warped out. In
        # this case, the object must be destroyed. This does not
        # happen if a non-weakened reference is kept in Tk Tree.
        weaken( $data[1] );

        my $elt_type = $node->element_type($elt);
        my $eltmode  = $elt_mode{$elt_type};
        if ( $tkt->infoExists($newpath) ) {
            $eltmode = $tkt->getmode($newpath);    # will reuse mode below
        }
        else {
            my @opt = $prevpath ? ( -after => $prevpath ) : ( -at => 0 );
            $logger->trace("disp_obj_elt add $newpath mode $eltmode type $elt_type");
            $tkt->add( $newpath, -data => \@data, @opt );
            $tkt->itemCreate( $newpath, 0, -text => $elt );
            $tkt->setmode( $newpath => $eltmode );
        }

        my $elt_loc = $node_loc ? $node_loc . ' ' . $elt : $elt;

        $cw->setmode( 'node', $newpath, $eltmode, $elt_loc, $opening, $actions, $scan_sub );

        my $obj = $node->fetch_element($elt);

        if ($elt_type =~ 'node') {
            $tkt->itemCreate( $newpath, 2, -text => $obj->fetch_gist );
        }

        if ( $elt_type eq 'hash' ) {
            $cw->update_hash_image( $obj, $newpath );
        }

        if ($elt_type eq 'hash' or $elt_type eq 'list') {
            my $size = $obj->fetch_size;
            $tkt->entryconfigure($newpath, -text => "$elt [$size]");
        }

        $cw->show_single_list_value ($tkt, $obj, $newpath,  $tkt->getmode($newpath) eq 'open' ? 1 : 0);

        if ( $force_display_path and $force_display_path eq $elt_loc ) {
            $cw->force_display($newpath, $elt_loc);
        }
        if (not $force_display_path and $cw->{location} eq $elt_loc) {
            $cw->force_display($newpath, $elt_loc);
        }

        $prevpath = $newpath;
    }
}

sub force_display {
    my ($cw, $path, $loc) = @_;
    $logger->debug("force_display called on $path, location $loc");
    my $tree = $cw->{tktree};
    $tree->selectionClear();
    $tree->anchorClear();
    $tree->see($path);
    $tree->anchorSet($path);
    $cw->{location} = $loc;
}

# show a list like a leaf value when the list contains *one* item
sub show_single_list_value {
    my ($cw, $tkt, $obj, $path, $show) = @_;

lib/Config/Model/TkUI.pm  view on Meta::CPAN


    # need to keep track myself of previous sibling as
    # $tkt->entrycget($path,'-after') dies
    # and $tkt->info('prev',$path) return the path above in the displayed tree, which
    # is not necessarily a sibling :-(
    my $prev_sibling = '';
    my %tk_previous_path;
    foreach ( $tkt->info( 'children', $path ) ) {
        $tk_previous_path{$_} = $prev_sibling;
        $prev_sibling = $_;
    }

    my $prevpath = '';
    foreach my $idx (@idx) {
        my $newpath  = $path . '.' . to_path($idx);
        my $scan_sub = sub {
            $scanner->scan_hash(
                [ $newpath, $cw,  $opening, $actions, $force_display_path ],
                $node, $element_name, $idx
            );
        };

        my $eltmode = $elt_mode{$elt_type};
        my $sub_elt = $elt->fetch_with_id($idx);

        # check for display order mismatch
        if ( $tkt->infoExists($newpath) ) {
            if ( $prevpath ne $tk_previous_path{$newpath} ) {
                $logger->trace(
                    "disp_hash deleting mismatching $newpath mode $eltmode cargo_type $elt_type");
                $tkt->delete( entry => $newpath );
            }
        }

        # check for content mismatch
        if ( $tkt->infoExists($newpath) ) {
            my $previous_data = $tkt->info( data => $newpath );

            # $previous_data is an object (or an empty string to avoid warnings)
            my $previous_elt = $previous_data->[1] || '';
            $eltmode = $tkt->getmode($newpath);    # will reuse mode below
            $logger->trace( "disp_hash reuse $newpath mode $eltmode cargo_type $elt_type"
                    . " obj $previous_elt (expect $sub_elt)" );

            # string comparison of objects is intentional to check that the tree
            # refers to the correct Config::Model object
            if ( $sub_elt ne $previous_elt ) {
                $logger->trace( "disp_hash delete $newpath mode $eltmode (got "
                        . "$previous_elt expected $sub_elt)" );

                # wrong order, delete the entry
                $tkt->delete( entry => $newpath );
            }
        }

        if ( not $tkt->infoExists($newpath) ) {
            my @opt = $prevpath ? ( -after => $prevpath ) : ( -at => 0 );
            $logger->trace(
                "disp_hash add $newpath mode $eltmode cargo_type $elt_type" . " elt $sub_elt" );
            my @data = ( $scan_sub, $sub_elt );
            weaken( $data[1] );
            $tkt->add( $newpath, -data => \@data, @opt );
            $tkt->itemCreate( $newpath, 0, -text => $node->shorten_idx($idx) );
            $tkt->setmode( $newpath => $eltmode );
        }

        # update the node gist
        my $gist = $elt_type =~ /node/ ? $elt->fetch_with_id($idx)->fetch_gist : '';
        $tkt->itemCreate( $newpath, 2, -text => $gist );

        my $elt_loc = $sub_elt->location;

        # hide new entry if hash is not yet opened
        $cw->setmode( 'hash', $newpath, $eltmode, $elt_loc, $opening, $actions, $scan_sub );

        if ( $force_display_path and $force_display_path eq $elt_loc ) {
            $cw->force_display($newpath, $elt_loc)
        }

        $prevpath = $newpath;
    }
}

sub update_hash_image {
    my ( $cw, $elt, $path ) = @_;
    my $tkt = $cw->{tktree};

    # check hash status and set warning image if necessary
    my $img;
    {
        no warnings qw/uninitialized/;
        $img = $warn_img if $elt->warning_msg;
    }

    if ( defined $img ) {
        $tkt->itemCreate( $path, 1, -itemtype => 'image', -image => $img );
    }
    else {
        $tkt->itemDelete( $path, 1 ) if $tkt->itemExists( $path, 1 );
    }
}

sub setmode {
    my ( $cw, $type, $newpath, $eltmode, $elt_loc, $opening, $actions, $scan_sub ) = @_;
    my $tkt = $cw->{tktree};

    $actions->{$elt_loc} //= '';
    my $force_open = $actions->{$elt_loc} eq 'show' ? 1 : 0;
    my $force_close = $actions->{$elt_loc} eq 'hide' ? 1 : 0;

    $logger->trace( "$type: elt_loc '$elt_loc', opening $opening "
                        . "eltmode $eltmode force_open $force_open");

    if ( not $force_close and ($eltmode ne 'open' or $force_open or $opening )) {
        $tkt->show( -entry => $newpath );

        # counter-intuitive: want to display [-] if force opening and not leaf item
        $tkt->setmode( $newpath => 'close' ) if ( $force_open and $eltmode ne 'none' );
    }
    elsif ($force_close and $eltmode eq 'open') {
        $tkt->hide( -entry => $newpath );

lib/Config/Model/TkUI.pm  view on Meta::CPAN

    my $scanner = Config::Model::ObjTreeScanner->new(
        check    => 'no',

        # node callback
        node_content_cb => \&disp_obj_elt,

        # element callback
        list_element_cb       => \&disp_hash,
        check_list_element_cb => \&disp_check_list,
        hash_element_cb       => \&disp_hash,
        node_element_cb       => \&disp_node,

        # leaf callback
        leaf_cb            => \&disp_leaf,
    );

    $cw->{scanner} = $scanner;

}

my %widget_table = (
    edit => {
        leaf       => 'ConfigModelLeafEditor',
        check_list => 'ConfigModelCheckListEditor',
        list       => 'ConfigModelListEditor',
        hash       => 'ConfigModelHashEditor',
        node       => 'ConfigModelNodeEditor',
    },
    view => {
        leaf       => 'ConfigModelLeafViewer',
        check_list => 'ConfigModelCheckListViewer',
        list       => 'ConfigModelListViewer',
        hash       => 'ConfigModelHashViewer',
        node       => 'ConfigModelNodeViewer',
    },
);

sub create_element_widget {
    my $cw        = shift;
    my $mode      = shift;
    my $tree_path = shift;    # optional
    my $obj       = shift;    # optional if tree is not opened to path

    my $tree = $cw->{tktree};

    unless ( defined $tree_path ) {
        # pointery and rooty are common widget method and must called on
        # the right widget to give accurate results
        $tree_path = $tree->nearest( $tree->pointery - $tree->rooty );
    }

    if ( $tree->info( exists => $tree_path ) ) {
        $tree->selectionClear();    # clear all
        $tree->selectionSet($tree_path);
        my $data_ref = $tree->infoData($tree_path);
        unless ( defined $data_ref->[1] ) {
            $cw->reload;
            return;
        }
        $obj = $data_ref->[1];
        weaken($obj);
    }

    my $loc  = $obj->location;
    my $type = $obj->get_type;
    $logger->trace("item $loc to $mode (type $type)");

    my $e_frame = $cw->Subwidget('ed_frame');

    # cleanup existing widget contained in this frame
    delete $cw->{editor};
    map { $_->destroy if Tk::Exists($_) } $e_frame->children;

    my $widget = $widget_table{$mode}{$type}
        || die "Cannot find $mode widget for type $type";
    my $weak_cw = $cw;
    weaken($weak_cw);
    my @store = $mode eq 'edit' ? ( -store_cb => sub { $weak_cw->reload(@_) } ) : ();
    $cw->{current_mode} = $mode;

    my $tk_font = $cw->cget('-font');
    $cw->{editor} = $e_frame->$widget(
        -item => $obj,
        -path => $tree_path,
        -font => $tk_font,
        @store,
    );

    $cw->{editor}->ConfigSpecs( -font => ['DESCENDANTS', 'font','Font', $tk_font ]);

    $cw->{editor}->pack( -expand => 1, -fill => 'both' );
    return $cw->{editor};
}

sub edit_copy {
    my $cw  = shift;
    my $tkt = $cw->{tktree};

    my @selected = @_ ? @_ : $tkt->info('selection');

    #print "edit_copy @selected\n";
    my @res;

    foreach my $selection (@selected) {
        my $data_ref = $tkt->infoData($selection);

        my $cfg_elt   = $data_ref->[1];
        my $type      = $cfg_elt->get_type;
        my $cfg_class = $type eq 'node' ? $cfg_elt->config_class_name : '';

        #print "edit_copy '",$cfg_elt->location, "' type '$type' class '$cfg_class'\n";

        push @res,
            [
            $cfg_elt->element_name, $cfg_elt->index_value, $cfg_elt->composite_name,
            $type,                  $cfg_class,            $cfg_elt->dump_as_data() ];
    }

    $cw->{cut_buffer} = \@res;

    #use Data::Dumper; print "cut_buffer: ", Dumper( \@res ) ,"\n";

    return \@res;    # for tests
}

sub edit_paste {
    my $cw  = shift;
    my $tkt = $cw->{tktree};

    my @selected = @_ ? @_ : $tkt->info('selection');

    return unless @selected;

    #print "edit_paste in @selected\n";
    my @res;

    my $selection = $selected[0];



( run in 1.813 second using v1.01-cache-2.11-cpan-39bf76dae61 )