Tk::MListbox doesn't expand - perl

I would like to create a Tk application with a MListbox to display some data. If there is too much information, I would like that a scrollbar appears.
My problem is that the MListbox doesn't fill all the available space. There is a blank space on the right. It doesn't look very nice.
Is it possible to solve this problem? Or should I use another widget? (TableMatrix seems interesting but I can't download it). I chose MLlistbox because I want to be able to hide some columns and change the size of each column.
This is the code I had so far:
my $frameDocuments = $mw->Frame(-background => '#CCCCFF');
$documentsListbox = $frameDocuments->Scrolled(
'MListbox',
-scrollbars => 'osoe',
-columns => [
[-text => 'Name'], [-text => 'Path'], [-text => 'Format'],
[-text => 'Loader Type'], [-text => 'Cache directory']
],
-resizeable => 1,
-moveable => 1,
-sortable => 1,
-selectmode => 'browse',
);
$frameDocuments->pack(-anchor => "n",-expand => "1",-fill => "both",-side => "top");
$documentsListbox->pack(-anchor => "n",-expand => "1",-fill => "both",-side => "top");

It seems like the Tk::MListbox does not resize its own columns when the window width becomes larger than the sum of the column widths. Seems like a bug, maybe you should report it?
Anyway, you can try work around it by using columnPack function. According to the documentation:
$ml->columnPack(array)
Repacks all columns in the MListbox widget
according to the specification in array. Each element in array is a
string on the format index:width. index is a column index, width
defines the columns width in pixels (may be omitted). The columns are
packed left to right in the order specified by by array. Columns not
specified in array will be hidden.
Here is an example1 where I maximize the window to fill the whole screen, and then compute the column widths:
#! /usr/bin/env perl
use strict;
use warnings;
use Tk;
use Tk::MListbox;
my $mw = MainWindow->new();
my $frameDocuments = $mw->Frame(-background => '#CCCCFF');
my #columns = (
[-text => 'Name'],
[-text => 'Path'],
[-text => 'Format'],
[-text => 'Loader Type'],
[-text => 'Cache directory']
);
my $numCols = scalar #columns;
my $documentsListbox = $frameDocuments->Scrolled(
'MListbox',
-scrollbars => 'osoe',
-columns => \#columns,
-resizeable => 1,
-moveable => 1,
-sortable => 1,
-selectmode => 'browse',
);
$frameDocuments->pack(
-anchor => "n",
-expand => "1",
-fill => "both",
-side => "top"
);
$documentsListbox->pack(
-anchor => "n",
-expand => "1",
-fill => "both",
-side => "top"
);
my $screenHeight = $mw->screenheight;
my $screenWidth = $mw->screenwidth;
$mw->geometry( sprintf "%dx%d+0+0", $screenWidth, $screenHeight );
my $colWidth = int( $screenWidth / $numCols );
my #ar = map { "$_:$colWidth" } 0 .. ($numCols - 1);
$documentsListbox->columnPack(#ar);
MainLoop;
Resulting window:
Footnotes:
I used camelCase for variable names in the code snippet since you already used it in your question. Note that snake_case is more common in Perl.

Related

Stringify Struct in Perl

In my program, I have a section of code that updates a list box depending on the value of another list box.
The code that does this looks a little like this.
$listBox1->bind('<<ListboxSelect>>' => sub {
$listBox2->delete(0, 'end');
for(#{$hashOfArraysOfStruct{$listBox1->get($listBox1->curselection)}}) {
$listBox2->insert('end', $_->name);
}
});
This works fine. However, I have found it easier to simply use a list and manipulate the list on the <<ListboxSelect>>. I have bound this list to the list box using -listvariable.
The code to do this looks a bit like
$listBox1->bind('<<ListboxSelect>>' => sub {
#updateableList = #{$hashOfArraysOfStruct{$listBox1->get($listBox1->curselection)}};
});
The problem with this approach is, as hashOfArraysOfStruct, contains structures, the list box contains values such as MyStruct=HASH(0x31d7e3c).
Is there any way to display the name variable of the struct MyStruct without looping through the whole array and individually inserting each result into the list box?
MCVE
use strict;
use warnings;
use Tk;
use Class::Struct;
struct MyStruct => {
name => '$',
group => '$'
};
my %hashOfArraysOfStruct = (
A => [
MyStruct->new(name => 'Phil', group => 'A'),
MyStruct->new(name => 'Ian', group => 'A'),
MyStruct->new(name => 'George', group => 'A')
],
B => [
MyStruct->new(name => 'Mac', group => 'B'),
MyStruct->new(name => 'Will', group => 'B')
],
C => [
MyStruct->new(name => 'Cath', group => 'C'),
MyStruct->new(name => 'Thom', group => 'C'),
MyStruct->new(name => 'Richard', group => 'C'),
MyStruct->new(name => 'Paul', group => 'C'),
MyStruct->new(name => 'Nick', group => 'C')
]
);
my $mainWindow = MainWindow->new();
my #listOne = sort(keys %hashOfArraysOfStruct);
############################################
#Route One
#Less efficient as has to loop through all the values
#But it displays the name variable of MyStruct
my $listBox1 = $mainWindow->Scrolled("Listbox", -scrollbars => "osoe", -selectmode => "single", -listvariable => \#listOne)->pack;
my $listBox2 = $mainWindow->Scrolled("Listbox", -scrollbars => "osoe", -selectmode => "single")->pack;
$listBox1->bind('<<ListboxSelect>>' => sub {
$listBox2->delete(0, 'end');
for(#{$hashOfArraysOfStruct{$listBox1->get($listBox1->curselection)}}) {
$listBox2->insert('end', $_->name);
}
});
############################################
############################################
#Route Two
#Works but displays in the form of MyStruct=HASH(0x31d7e3c)
#my #updateableList;
#my $listBox1 = $mainWindow->Scrolled("Listbox", -scrollbars => "osoe", -selectmode => "single", -listvariable => \#listOne)->pack;
#my $listBox2 = $mainWindow->Scrolled("Listbox", -scrollbars => "osoe", -selectmode => "single", -listvariable => \#updateableList)->pack;
#$listBox1->bind('<<ListboxSelect>>' => sub {
# #updateableList = #{$hashOfArraysOfStruct{$listBox1->get($listBox1->curselection)}};
#});
############################################
############################################
#What I would like to happen
#I would like to use route two but when the struct is displayed
#in the list box, instead of being in Route Twos format, it should
#display the name variable of MyStruct.
############################################
MainLoop;
Massive Edit
Change
#updateableList = #{$hashOfArraysOfStruct{$listBox1->get($listBox1->curselection)}};
to
#updateableList = map { $_->name() } #{$hashOfArraysOfStruct{$listBox1->get($listBox1->curselection)}};
To extract the list of names from the list of structs;

Rearrange data in a PDF file

I am using Perl to generate a PDF file with the
PDF::API2 module.
I am having difficulty writing data to the file in the required format.
Currently this is how it appears in the PDF file.
Here's my script
my %data = (
'1' => {
'SEQUENCE' => '1',
'NAME' => 'John',
'ADDR1' => 'Road 1',
'GRADE' => '5'
},
'2' => {
'SEQUENCE' => '2',
'NAME' => 'Smith',
'ADDR1' => 'Road 2',
'GRADE' => '6'
}
);
...
...
my #rows = qw( NAME ADDR1 GRADE );
for my $id (sort keys %data){
push #tbl, [ #{$data{$id}}{#rows} ];
($name, $addr, $grade) = ($data{$id}{'NAME'}, $data{$id}{'ADDR1'}, $data{$id}{'GRADE'});
}
...
...
my $pdftable = new PDF::Table;
$pdftable->table(
$pdf,
$page,
\#tbl,
x => 50,
w => 400,
start_y => 630,
start_h => 630,
next_y => 630,
next_h => 630,
padding => 5,
border => 1,
padding_right => 10,
);
$pdf->saveas();
But I want it to to appear like this:
I know I have to make a modification to #tbl data, but how?
The PDF::Table table function takes an "array or arrays" as its third argument. You currently have it strucutred like:
my #tbl = ( [$name,$addr,$grade], [...] )
Each nested array (what's inside the [] brackets) is a new row of the table, and the elements to that array are the cells of that row. So instead, you want it structured:
my #tbl = ( ['NAME',$name], ['ADDR1',$addr], ['GRADE',$grade], [...] )
To construct it using your %data structure:
for my $id (sort keys %data) {
push #tbl, [$_, $data{$id}->{$_}] for qw/NAME ADDR1 GRADE/;
push #tbl, [qw/- -/]; # to add a blank row as a separator
}

Perl Mongo find object Id

You would think it is a simple thing. I have a list of object id's that are in my collection. I would like to get a single record based on an object id. Have Googled, but nothing helpful.
So I have object id: 5106c7703abc120a04070b34
my $client = MongoDB::MongoClient->new;
my $db = $client->get_database( 'myDatabase' );
my $id_find = $db->get_collection('mycollection')->find({},{_id => MongoDB::OID->new(value => "5106c7703abc120a04070b34")});
print Dumper $id_find;
This prints:
$VAR1 = bless( {
'_skip' => 0,
'_ns' => 'MindCrowd_test.Users',
'_grrrr' => 0,
'partial' => 0,
'_query' => {},
'_tailable' => 0,
'_client' => bless( {
'w' => 1,
'query_timeout' => 30000,
'find_master' => 0,
'_servers' => {},
'sasl' => 0,
'wtimeout' => 1000,
'j' => 0,
'timeout' => 20000,
'sasl_mechanism' => 'GSSAPI',
'auto_connect' => 1,
'auto_reconnect' => 1,
'db_name' => 'admin',
'ssl' => 0,
'ts' => 0,
'inflate_dbrefs' => 1,
'port' => 27017,
'host' => 'mongodb://localhost:27017',
'dt_type' => 'DateTime',
'max_bson_size' => 16777216
}, 'MongoDB::MongoClient' ),
'_limit' => 0,
'slave_okay' => 0,
'_request_id' => 0,
'immortal' => 0,
'started_iterating' => 0
}, 'MongoDB::Cursor' );
I have tried different verions of the above find. All of them fail to compile:
$mongo->my_db->my_collection(find({_id => "ObjectId(4d2a0fae9e0a3b4b32f70000"}));
$mongo->my_db->my_collection(
find({ _id => MongoDB::OID->new(value => "4d2a0fae9e0a3b4b32f70000")})
);
NONE of them work. How do I find (findone) a single record using the object id??
the find methods returns a Cursor object for iterating through. If you only want one record use the find_one method which returns a value.
my $client = MongoDB::MongoClient->new;
my $db = $client->get_database( 'myDatabase' );
my $id_find = $db->get_collection('mycollection')->find_one({_id => MongoDB::OID->new(value => "5106c7703abc120a04070b34")});
print Dumper $id_find;
The answer to this has changed. MongoDB::OID has been deprecated, replaced by BSON::OID, which does not have a method that allows you to pass in the 24-byte hex string that you have. Here's what you have to do these days:
my $id = "5c7463277fc2198b64654feb";
my $oid = BSON::OID->new(oid => pack('H24', $id));
my $result = $db->get_collection('mycollection')->find_id($oid);
pack creates a 12-byte binary sequence from the 24-bytes of hexadecimal data you have in $id. This is what BSON::OID is expecting, and then the perl driver constructs the correct filter for you in the background.

How can I change a log4perl appender's filters at run time?

I'v been trying to figure out if I can change an appender's filter at run-time that I've defined via a configuration file.
log4perl.filter.M1 = Log::Log4perl::Filter::LevelMatch
log4perl.filter.M2 = Log::Log4perl::Filter::LevelMatch
log4perl.filter.M1.LevelToMatch = INFO
log4perl.filter.M1.AcceptOnMatch = true
log4perl.filter.M2.LevelToMatch = WARN
log4perl.filter.M2.AcceptOnMatch = true
log4perl.filter.MyBoolean0 = Log::Log4perl::Filter::Boolean
log4perl.filter.MyBoolean0.logic = M1
log4perl.filter.MyBoolean1 = Log::Log4perl::Filter::Boolean
log4perl.filter.MyBoolean1.logic = M1 || M2
log4perl.appender.SCREEN.Filter = MyBoolean0
I'd like to change this filter from MyBoolean0 for the SCREEN to MyBoolean1, but do it after my program has started running.
Poking at the APPENDER_BY_NAME hash for SCREEN using Data::Dumper shows the following:
$VAR1 = bless( {
'appender' => bless( {
'Filter' => 'MyBoolean0',
'color' => {
...
...
'filter' => bless( {·
'params' => {·
'M3' => bless( {·
'LevelToMatch' => 'ERROR',
'name' => 'M3',
'AcceptOnMatch' => 1
}, 'Log::Log4perl::Filter::LevelMatch' ),
'M1' => bless( {·
'LevelToMatch' => 'INFO',
'name' => 'M1',
'AcceptOnMatch' => 1
}, 'Log::Log4perl::Filter::LevelMatch' ),
'M2' => bless( {·
'LevelToMatch' => 'WARN',
'name' => 'M2',
'AcceptOnMatch' => 1
}, 'Log::Log4perl::Filter::LevelMatch' )
},
'name' => 'MyBoolean0',
'eval_func' => sub { "DUMMY" },
'logic' => 'M1 || M2 || M3'
}, 'Log::Log4perl::Filter::Boolean' ),
'warp_message' => undef,
'name' => 'SCREEN'
}, 'Log::Log4perl::Appender' );
But mucking with this HASH seems hackish to me. Is there a better way to change an appender's filters?
You may use undocumented appender's property filter:
$Log::Log4perl::Logger::APPENDER_BY_NAME{'SCREEN'}->filter(
Log::Log4perl::Filter::by_name('MyBoolean1')
);
Also you may use two appenders:
log4perl.appender.SCREEN0.Filter = MyBoolean0
log4perl.appender.SCREEN1.Filter = MyBoolean1
And change it in runtime:
$logger->remove_appender('SCREEN0', 1);
$logger->add_appender(
Log::Log4perl::Config::create_appender_instance(
$Log::Log4perl::Config::OLD_CONFIG,
'SCREEN1',
\%Log::Log4perl::Logger::APPENDER_BY_NAME
)
);

strange bug accessing a variable in template toolkit

I'm struggling with a bug, that I can't nail down.
I have a function that takes a postcode, does a lookup, and returns a latitude, longitude and area name.
for example, pass it AD300 it returns (something like) 42.6, 1.55, ordino - it works very well.
The function is called like this:
my ($lat, $lng, $area) = $object->release();
The return values are fine, and I can print them in perl with a warn
warn "Area $area, $rellat, $rellng";
This works fine. "Area Ordino, 42.6, 1.55"
I then take one of these values, say $area, add it to a hash of data, and pass it to a web page where it is preprocessed via TT (as I do successfully with a load of other variables).
I'm assigning the value to the hash in the normal way. e.g.
$hash->{'area'} = $area;
Here is where the fun begins. When I try to reference the value in TT e.g. [% hash.area %]
I don't get "Ordino" printed on the web page, I'm told I've passed an Array reference to TT.
After a little debugging, I've found that my hash variable hash.area, is somehow referencing an array (according to TT) holding the three values that I've returned from the subroutine "release". I.e.
hash.area = [42.6, 1.55, ordino] according to TT.
That is, to get the value "Ordino" within the web page, I have to access [% hash.area.2 %].
Further, I can set $hash->{'area'} to equal any of the variables, $lat, $lng, or $area and get the same behavior. TT believes all three variables reference the same array. that is
$lat = $lng = $area = [42.6, 1.55, ordino] according to TT
This is bizare, I can happily print the variables in perl and they appears as normal - not an array. I've tried dumping the hash with dumper, no array, everything is fine. Yet somehow, TT is finding an array. It's doing my head in.
The site is quite large, with a lot of pages and I happily pass variables and hashes via TT to web pages all the time, and have been for 4 years now. I've never seen this. On other pages, I even pass exactly the same output from the "release" method and it is processed correctly.
I don't think my TT processing code is the problem, however the following is relevant.
my $tt = Template->new({
INCLUDE_PATH => [ #$template_directories ],
COMPILE_EXT => '.ttc',
COMPILE_DIR => '/tmp/ttc',
FILTERS => YMGN::View->filters,
PLUGIN_BASE => [ 'YMGN::V::TT::Plugins' ],
EVAL_PERL => 1
});
$self->{tt} = $tt;
$self->{template_directories} = $template_directories;
$self->{output} = $params->{output} || undef;
$self->{data} = $params->{data} || [];
The above creates a new tt object and is part of the "new" function (refed below).
"data" contains the hash. "output" holds the processed template ready to send to users browser. We call new (above), process the data and create the output with the code below.
sub process {
my $self = shift;
my $params = shift;
if (!ref $self || !exists $self->{tt}) {
my $class = $self;
$self = $class->new($params);
}
if (!$self->{output}) {
die "You need to specify output";
}
delete $self->{error};
$self->y->utils->untaint(\$self->{template});
my $rv = $self->{tt}->process(
$self->{template},
$self->{data},
$self->{output},
binmode => ':utf8',
);
if (!$rv) {
warn $self->{tt}->error();
return {
error => $self->{tt}->error(),
};
}
return 0;
}
All of the above is sanitised because there is a lot of other stuff going on.
I believe what's important is that the data going in looks correct, here is a full dump of the complete data that is being processed by tt (at the point of processing). The thing that is causing the problem is bubbles->[*]->{'release'} (note, that release == area in the data. The name was changed for unrelated reasons). As you can see, dumper thinks it's a string. TT deals with everything else fine.
data $VAR1 = {
'system' => {
system stuff
},
'features' => {
site feature config
},
'message_count' => '0',
'bubbles' => [
bless( {
'history' => [
{
'creator' => '73',
'points' => '10',
'screenname' => 'sarah10',
'classname' => 'Flootit::M::Bubbles',
'id' => '1378',
'updated' => '1352050471',
'type' => 'teleport',
'label' => 'teleport',
'class' => 'Flootit::M::Bubbles'
}
],
'creator' => '6',
'release' => 'Escaldes-Engordany',
'image' => 'http://six.flooting.com/files/833/7888.png',
'pop_time' => '1352050644',
'y' => $VAR1->{'y'},
'taken_by' => '0',
'city' => '3',
'title' => 'hey a new bubble',
'id' => '566',
'class' => 'Flootit::M::Bubbles',
'prize' => 'go for it kids'
}, 'Flootit::M::Bubbles' ),
bless( {
'history' => [
{
'creator' => '6',
'points' => '10',
'screenname' => 'sarah20',
'classname' => 'Flootit::M::Bubbles',
'id' => '1723',
'updated' => '1349548017',
'type' => 'teleport',
'label' => 'teleport',
'class' => 'Flootit::M::Bubbles'
},
{
'creator' => '6',
'points' => '5',
'screenname' => 'sarah20',
'classname' => 'Flootit::M::Bubbles',
'id' => '1732',
'updated' => '1349547952',
'type' => 'blow',
'label' => 'blow',
'class' => 'Flootit::M::Bubbles'
}
],
'creator' => '89',
'release' => 'Ordino',
'image' => 'http://six.flooting.com/files/1651/8035.png',
'pop_time' => '1351203843',
'y' => $VAR1->{'y'},
'taken_by' => '0',
'city' => '3',
'title' => 'test4',
'id' => '1780',
'class' => 'Flootit::M::Bubbles',
'prize' => 'asdfasdf dsadsasdfasdfasdf'
}, 'Flootit::M::Bubbles' ),
bless( {
'history' => [],
'creator' => '6',
'release' => 'Andorra la Vella',
'image' => 'http://six.flooting.com/files/1671/8042.png',
'pop_time' => '0',
'y' => $VAR1->{'y'},
'taken_by' => '0',
'city' => '3',
'title' => 'Pretty flowers, tres joli',
'id' => '1797',
'class' => 'Flootit::M::Bubbles',
'prize' => 'With lots of pretty pictures'
}, 'Flootit::M::Bubbles' ),
bless( {
'history' => [],
'creator' => '6',
'release' => 'Hillrise Ward',
'image' => 'http://six.flooting.com/files/1509/8003.png',
'pop_time' => '0',
'y' => $VAR1->{'y'},
'taken_by' => '0',
'city' => '3',
'title' => 'Test beats',
'id' => '1546',
'class' => 'Flootit::M::Bubbles',
'prize' => 'Sound great'
}, 'Flootit::M::Bubbles' )
]
};
What comes out after processing is this (in $output)
There is a
[% FOREACH floot IN bubbles %]
Floating around ARRAY(0xfaf5d448). from [% floot.release %]
if we make this [% floot.release.2 %] it gives the correct value.
All the other fields can be referenced correctly - go figure.
The code that puts "bubbles" together is;
my $bubbles = $y->model('Bubbles')->search(['type' => 'golden', 'image' => '!NULL',
'bubble_prizes' => ['p', { 'p.bubble' => 'self.id'}], ], {
order_by => '(created>CURRENT_DATE() AND thumbsup+thumbsdown<10) DESC, COALESCE(thumbsup,0)-COALESCE(thumbsdown,0) DESC, pop_time DESC',
count => 10,
fields => ['p.title as title', 'p.prize as prize', 'city', 'taken_by', 'pop_time', 'id', 'creator'],
});
for (my $i=0; $i<#$bubbles; $i++) {
# Find specified bubbles (see below for when not found here)
my ($rellat, $rellng, $area) = $bubbles->[$i]->release() ;
$bubbles->[$i]->{'release'} = $area;
}
}
The controller then takes $bubble, bundles it up with session / site data, puts it inside an anonymous hash (as you can see in the data above) and passes it to view for processing.
The code for release is :
sub release {
my $self = shift;
my $postcode = $self->y->model('Prizes')->find({bubble => $self->id})->postcode;
my ( $user_lat, $user_long, $region_name );
if($postcode)
{
( $user_lat, $user_long, $region_name ) = $self->y->api('Location')->from_postcode($postcode);
return ( $user_lat, $user_long, $region_name );
}
}
API::Location is quite large, however the relevant lines are;
$postcode_record = $self->y->model('GeoData')->find( {
source => "ALL_COUNTRIES_POSTCODES",
country => $country_code,
sourceid => $postcode, } );
return ( $postcode_record->latitude, $postcode_record->longitude, $postcode_record->town );
The data dumps I've shown you are taken from inside TT.pm (part of view).
So, any ideas what might be going on or where to start? What can I do to try and debug this further? I'm out of ideas.
Maybe it's because $area is a blessed object; Try this to convert to a scalar string :
$string = ''.$area;
# e.g.
$hash->{'area'} = ''.$area;
Following #Moritz comment, to check that $area is blessed:
print ref($area);
use Data::Dumper; warn Dumper($area);
And q{""} overloaded:
print defined ${ref($area).'::'}{'(""'};
EDIT
sub release can return
- undef if $postcode evaluate to false
- a list but as it is used in as scalar context returns the last argument $region_name like parenthesized list (comma expression)
sub release {
my $self = shift;
my $postcode = $self->y->model('Prizes')->find({bubble => $self->id})->postcode;
my ( $user_lat, $user_long, $region_name );
if($postcode)
{
( $user_lat, $user_long, $region_name ) = $self->y->api('Location')->from_postcode($postcode);
return ( $user_lat, $user_long, $region_name );
}
}
It will be relevant to Dump $region_name or $area, or to look at from_postcode.
I found that the problem went away on other development servers and the production server.
I therefore tried uninstalling and reinstalling TT, however that didn't help.
As it appears it's an environment issue on my dev server, so I am retiring the box and starting a new one.