File Coverage

blib/lib/Quiz/Flashcards.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Quiz::Flashcards;
2 2     2   35916 use warnings;
  2         4  
  2         67  
3 2     2   12 use strict;
  2         3  
  2         64  
4            
5 2     2   46 use 5.010;
  2         26  
  2         85  
6            
7 2     2   10 use base 'Exporter';
  2         3  
  2         259  
8            
9             our @EXPORT = (qw( run_flashcard_app ));
10            
11 2     2   10 use Carp qw( confess );
  2         4  
  2         116  
12 2     2   2046 use utf8;
  2         20  
  2         11  
13 2     2   1868 use English qw(-no_match_vars);
  2         7394  
  2         11  
14            
15 2     2   1905 use Wx;
  0            
  0            
16            
17             run_flashcard_app() unless caller();
18            
19             =head1 NAME
20            
21             Quiz::Flashcards - Cross-platform modular flashcard GUI application
22            
23             =cut
24            
25             our $VERSION = '0.04'; # define version
26            
27             =head1 DESCRIPTION
28            
29             Created out of the need to aid in language studies while being able to quickly adapt the program for a higher learning efficiency than most showy flashcard applications allow. This application focuses not on teaching new material, but on training and reinforcing already learned material.
30            
31             It uses wxPerl for the GUI, which should make it work on most major desktop platforms. Additionally it stores data about the user's certainty and speed in answers in a SQLite database located in the user's data directory.
32            
33             Flashcard sets as well as additional data like sound files to go along with the sets will be available as seperate modules in the Quiz::Flashcards::Sets:: and Quiz::Flashcards::Audiobanks:: namespaces.
34            
35             =head1 SYNOPSIS
36            
37             use Edu::Flashcards;
38             run_flashcard_app();
39            
40             =head1 FUNCTIONS
41            
42             =head2 run_flashcard_app
43            
44             Starts the application itself.
45            
46             =cut
47            
48             sub run_flashcard_app {
49             my $app = Quiz::Flashcards::App->new;
50             $app->MainLoop;
51             }
52            
53             =head1 AUTHOR
54            
55             Christian Walde, C<< >>
56            
57             =head1 BUGS
58            
59             Please report any bugs or feature requests to C, or through
60             the web interface at L. I will be notified, and then you'll
61             automatically be notified of progress on your bug as I make changes.
62            
63            
64            
65            
66             =head1 SUPPORT
67            
68             You can find the source code repository with public read access on Google Code.
69            
70             =over 4
71            
72             =item
73            
74             L
75            
76             =back
77            
78            
79             You can find documentation for this module with the perldoc command.
80            
81             perldoc Quiz::Flashcards
82            
83            
84             You can also look for information at:
85            
86             =over 4
87            
88             =item * RT: CPAN's request tracker
89            
90             L
91            
92             =item * AnnoCPAN: Annotated CPAN documentation
93            
94             L
95            
96             =item * CPAN Ratings
97            
98             L
99            
100             =item * Search CPAN
101            
102             L
103            
104             =back
105            
106            
107             =head1 RELATED
108            
109             L, L
110            
111             =head1 COPYRIGHT & LICENSE
112            
113             Copyright 2009 Christian Walde, all rights reserved.
114            
115             This program is free software; you can redistribute it and/or modify it
116             under the same terms as Perl itself.
117            
118            
119             =cut
120            
121             ################################################################################
122            
123             package Quiz::Flashcards::App;
124            
125             use base 'Wx::App';
126            
127             use strict;
128             use warnings;
129             use utf8;
130             use Time::HiRes qw( time );
131             use Wx::XRC;
132             use File::HomeDir;
133             use File::Spec::Functions;
134             use Module::Find;
135             use Wx qw(:everything);
136             use Wx::Event qw(:everything);
137             use File::ShareDir ':ALL';
138             use DBI;
139             use List::Util 'shuffle';
140            
141             my %el;
142            
143             sub OnInit {
144             my $self = shift;
145            
146             $self->load_gui;
147             $self->load_config;
148             $self->adjust_fonts;
149             $self->load_sets_into_selector;
150             $self->load_timers;
151             $self->load_waiting_animator;
152             $self->register_events;
153            
154             return 1;
155             }
156            
157             # Event Sub-Routines
158             ################################################################################
159            
160             sub load_set {
161             my ( $self, $event ) = @_;
162            
163             my $module = $event->GetString;
164             my $title = "Quiz::Flashcards - $module";
165            
166             $module =~ s/ -> /::/;
167            
168             eval " require Quiz::Flashcards::Sets::$module; import Quiz::Flashcards::Sets::$module; ";
169             confess $@ if $@;
170            
171             @{ $self->{set} } = get_set();
172             $self->{set_name} = $module;
173            
174             $self->setup_set_table;
175             $self->load_set_table;
176             $self->load_set_sounds;
177            
178             $el{frame}->SetTitle($title);
179             $el{start_next_button}->SetLabel("Start");
180            
181             $el{start_next_button}->Enable;
182             $el{set_status_toggle}->Enable;
183            
184             $self->hide_sizer_contents( $self->{multi_answer_sizer} );
185             $el{answer}->Hide;
186             $el{correct_answer}->Hide;
187             $el{answer_time}->SetLabel( '' );
188            
189             $el{question}->SetBackgroundColour(wxNullColour);
190             $el{question}->SetLabel( '' );
191             $el{question}->Show;
192             $el{start_next_button}->Show;
193             $el{answer_time}->Show;
194            
195             $el{start_next_button}->SetFocus;
196            
197             $self->{set_complexity} = 1;
198             $self->{set_part_certainty} = 0;
199             $self->update_set_status;
200            
201             $self->re_layout;
202             }
203            
204             sub toggle_set_status {
205             my ( $self, $event ) = @_;
206            
207             if ( $el{set_status}->IsShown ) {
208             $el{set_status}->Hide;
209             $el{set_status_summary}->Hide;
210             }
211            
212             elsif ( $el{set_status_summary}->IsShown ) {
213             $el{set_status}->Show;
214             }
215            
216             else {
217             $self->update_set_status;
218             $el{set_status_summary}->Show;
219             }
220            
221             $self->re_layout;
222             }
223            
224             sub start_next_clicked {
225             my ( $self, $event ) = @_;
226            
227             $self->select_current_question;
228             $self->update_ui_for_question;
229             }
230            
231             sub check_answer {
232             my ( $self, $event ) = @_;
233            
234             $el{question_timer}->Stop;
235            
236             my $certainty_modifier;
237            
238             if ( $el{answer}->GetValue eq $self->{curr_question}->{answer} ) {
239             my $answer_time = time - $self->{curr_question}->{time_start};
240             $el{answer_time}->SetLabel( sprintf( "%.1f s", $answer_time ) );
241             $certainty_modifier = 100;
242             $self->{curr_question}->{time_to_answer} += .2 * ( $answer_time - $self->{curr_question}->{time_to_answer} );
243             $self->{curr_question}->{time_to_answer} = $self->{curr_question}->{time_to_answer};
244             $el{correct_answer}->Show;
245             #$el{question_description}->Show;
246             $self->enable_start_next_button;
247             $el{question}->SetBackgroundColour(wxGREEN);
248             }
249             else {
250             $el{question}->SetBackgroundColour(wxRED);
251             $certainty_modifier = 0;
252             $el{correct_answer}->Show;
253             #$el{question_description}->Show;
254             $el{wrong_timer}->Start(1_000);
255            
256             for my $item ( @{ $self->{set} } ) {
257             next if $item->{answer} ne $el{answer}->GetValue;
258            
259             $item->{certainty} += .1 * ( $certainty_modifier - $item->{certainty} );
260             $item->{certainty} = $item->{certainty};
261            
262             $self->update_user_data_db($item);
263            
264             last;
265             }
266             }
267            
268             my $certainty_change = .2 * ( $certainty_modifier - $self->{curr_question}->{certainty} );
269             $self->{curr_question}->{certainty} += $certainty_change;
270             $self->{curr_question}->{last_seen} = int time;
271            
272             $self->update_user_data_db( $self->{curr_question} );
273            
274             $self->update_set_status;
275            
276             Wx::Sound->new( $self->{curr_question}->{audio_file_path} )->Play() if $self->{curr_question}->{audio_file_path};
277            
278             $el{animator}->Hide;
279             $el{set_selector}->Enable;
280             $el{answer}->Disable;
281            
282             $self->re_layout;
283             }
284            
285             sub evt_multi_answer_button {
286             my ( $self, $event ) = @_;
287            
288             my $answer = $event->GetEventObject->GetLabel;
289            
290             $el{answer}->SetValue( $answer );
291            
292             $self->disable_sizer_contents( $self->{multi_answer_sizer} );
293            
294             $self->check_answer;
295             }
296            
297             sub evt_process_keyboard {
298             my ( $self, $event ) = @_;
299            
300             my $choice = $event->GetKeyCode();
301            
302             given ( $choice ) {
303             when ( $_ == WXK_NUMPAD7 ) { $choice = 0 }
304             when ( $_ == WXK_NUMPAD8 ) { $choice = 1 }
305             when ( $_ == WXK_NUMPAD9 ) { $choice = 2 }
306             when ( $_ == WXK_NUMPAD4 ) { $choice = 3 }
307             when ( $_ == WXK_NUMPAD5 ) { $choice = 4 }
308             when ( $_ == WXK_NUMPAD6 ) { $choice = 5 }
309             when ( $_ == WXK_NUMPAD1 ) { $choice = 6 }
310             when ( $_ == WXK_NUMPAD2 ) { $choice = 7 }
311             when ( $_ == WXK_NUMPAD3 ) { $choice = 8 }
312             when ( $_ == WXK_NUMPAD0 ) { $choice = 10 }
313             }
314            
315             if ( defined $choice ) {
316             EVT_KEY_DOWN( $self, undef );
317            
318             my @children = $self->{multi_answer_sizer}->GetChildren;
319             $choice = $children[$choice]->GetWindow->GetLabel;
320            
321             $el{answer}->SetValue( $choice );
322            
323             $self->disable_sizer_contents( $self->{multi_answer_sizer} );
324            
325             $self->check_answer;
326             }
327             }
328            
329             # Event Helper Sub-Routines
330             ################################################################################
331            
332             sub enable_start_next_button {
333             my ($self) = @_;
334            
335             $el{wrong_timer}->Stop;
336             $el{start_next_button}->Enable;
337             $el{start_next_button}->SetFocus;
338             }
339            
340             sub setup_set_table {
341             my ($self) = @_;
342            
343             $self->{set_table} = "set_$self->{set_name}";
344             $self->{set_table} =~ s/::/_/;
345            
346             $self->{dbh}->do( "
347             CREATE TABLE IF NOT EXISTS $self->{set_table} (
348             id INTEGER NOT NULL PRIMARY KEY,
349             certainty INTEGER DEFAULT 0 NOT NULL,
350             time_to_answer REAL DEFAULT 10 NOT NULL,
351             last_seen INTEGER DEFAULT 0 NOT NULL
352             )
353             " );
354             }
355            
356             sub load_set_table {
357             my ($self) = @_;
358            
359             my $hash_ref = $self->{dbh}->selectall_hashref( "SELECT * FROM $self->{set_table};", 'id' );
360            
361             for my $id ( 0 .. $#{ $self->{set} } ) {
362             next if !defined $self->{set}->[$id];
363             my $set_entry = $self->{set}->[$id];
364             $set_entry->{id} = $id;
365             $set_entry->{certainty} = $hash_ref->{$id}->{certainty} || 0;
366             $set_entry->{time_to_answer} = $hash_ref->{$id}->{time_to_answer} || 10;
367             $set_entry->{last_seen} = $hash_ref->{$id}->{last_seen} || 0;
368             }
369             }
370            
371             sub load_set_sounds {
372             my ($self) = @_;
373            
374             for my $item ( @{ $self->{set} } ) {
375             next unless $item->{audiobank};
376             next unless $item->{audio_file};
377            
378             my $ab = $item->{audiobank};
379             $self->load_audiobank($ab) unless $self->{audiobanks}->{$ab};
380             $item->{audio_file_path} = $self->{audiobanks}->{$ab}->{ $item->{audio_file} };
381             }
382             }
383            
384             sub update_set_status {
385             my ( $self, $event ) = @_;
386             $el{set_status}->ClearAll;
387            
388             my %sum;
389             my $i = 0;
390             my $set_size = @{ $self->{set} };
391            
392             for my $item ( @{ $self->{set} } ) {
393             $el{set_status}->InsertStringItem(
394             $i++,
395             "$item->{question}: $item->{certainty} %, " . sprintf( "%.1f", $item->{time_to_answer} ) . " s"
396             );
397             $sum{certainty} += $item->{certainty};
398             $sum{time_to_answer} += $item->{time_to_answer};
399             }
400             my $title = $self->{set_name};
401             $title =~ s/::/ -> /;
402            
403             $self->calc_set_complexity;
404            
405             my $set_status_summary =
406             "$title\nCertainty = "
407             . sprintf( "%.2f", $sum{certainty} / $set_size )
408             . " Answer Time = "
409             . sprintf( "%.1f", $sum{time_to_answer} / $set_size )
410             . " Complexity = "
411             . sprintf( "%d/%d", $self->{set_complexity}, $self->{max_complexity} )
412             . " Part Certainty = "
413             . sprintf( "%.2f", $self->{set_part_certainty} );
414            
415             $el{set_status_summary}->SetLabel( $set_status_summary );
416             }
417            
418             sub calc_set_complexity {
419             my ( $self ) = @_;
420            
421             if( !$self->{max_complexity} ) {
422             $self->{max_complexity} = 1;
423            
424             for my $item ( @{ $self->{set} } ) {
425             $self->{max_complexity} = $item->{complexity} if $self->{max_complexity} < $item->{complexity};
426             }
427             }
428            
429             # calculate the certainty for each complexity group, starting with the largest
430             for my $c ( 1 .. $self->{max_complexity} ) {
431             my ($sum, $count, $done) = (0,0,0);
432             for my $item ( @{ $self->{set} } ) {
433             next if $item->{complexity} != $c;
434             $sum += $item->{certainty};
435             $count++;
436             }
437            
438             my $this_certainty = $sum/$count;
439             $done = 1 if (
440             ( $self->{set_part_certainty} > 50 and $this_certainty <= 50 )
441             or ( $this_certainty <= 50 and $c == 1 )
442             );
443            
444             $self->{set_complexity} = $c;
445             $self->{set_part_certainty} = $sum/$count;
446            
447             last if $done;
448             }
449            
450             }
451            
452             sub select_current_question {
453             my ($self) = @_;
454             my ( @choices, $margin );
455            
456             # remove items that have a too high complexity and make sure the last one isn't repeated immediately
457             for my $item ( @{ $self->{set} } ) {
458             next if $item->{complexity} > $self->{set_complexity};
459             next if $self->{curr_question} and $self->{curr_question}{question} eq $item->{question};
460            
461             push @choices, $item;
462             }
463            
464             # find items with low certainty that we haven't seen recently
465             @choices = trim_hash_array_by ( "certainty", \@choices );
466            
467             # find items with low certainty that we haven't seen recently
468             @choices = trim_hash_array_by ( "last_seen", \@choices );
469            
470             # find items with a high response time
471             @choices = trim_hash_array_by ( "time_to_answer", \@choices, 'reverse' );
472            
473             # pick random item from choices
474             my $pick = int( rand($#choices) );
475            
476             $self->{curr_question} = $choices[$pick];
477             }
478            
479             sub trim_hash_array_by {
480             my ($parameter, $array, $reverse) = @_;
481            
482             my @sort_array;
483             @sort_array = sort { $a->{$parameter} <=> $b->{$parameter} } @{$array};
484             @sort_array = reverse @sort_array if $reverse;
485            
486             my $margin = 0.33 * scalar @sort_array;
487             $margin = 4 if $margin < 4;
488             splice @sort_array, $margin;
489            
490             return @sort_array;
491             }
492            
493             sub update_ui_for_question {
494             my ($self) = @_;
495            
496             $el{question}->SetBackgroundColour(wxNullColour);
497             $el{set_selector}->Disable;
498             $el{start_next_button}->Disable;
499             $el{start_next_button}->SetLabel("Next");
500             $el{answer_time}->SetLabel('');
501             $el{correct_answer}->Hide;
502             $el{correct_answer}->SetLabel( $self->{curr_question}{answer} );
503             $el{animator}->Show;
504             $self->enable_sizer_contents( $self->{multi_answer_sizer} ) if ( $self->{curr_question}{answer_type} eq 'multi' );
505            
506             given ($self->{curr_question}{answer_type}) {
507             when ( "text" ) { $self->load_text_answer; }
508             when ( "multi" ) { $self->load_multi_answer; }
509             default { confess( "unknown answer type: $self->{curr_question}{answer_type}" ); }
510             }
511            
512             $el{question}->SetLabel( $self->{curr_question}{question} );
513             $self->re_layout;
514            
515             $self->{curr_question}->{time_start} = time;
516             $el{question_timer}->Start(10_000);
517             }
518            
519             sub load_text_answer {
520             my ($self) = @_;
521            
522             $self->hide_sizer_contents( $self->{multi_answer_sizer} );
523             $el{answer}->Show;
524             $el{answer}->SetValue('');
525             $el{answer}->Enable;
526             $el{answer}->SetFocus;
527             }
528            
529             sub load_multi_answer {
530             my ($self) = @_;
531            
532             my (@possible_answers, @answers);
533            
534             for my $answer ( @{ $self->{set} } ) {
535             next if $answer->{answer} eq $self->{curr_question}{answer};
536             next if $answer->{complexity} > $self->{set_complexity};
537            
538             push @possible_answers, $answer->{answer};
539             }
540            
541             push @answers, $self->{curr_question}{answer};
542             while ( @answers < 9 and @possible_answers > 0 ) {
543             my $pick = int( rand( $#possible_answers ) );
544             push @answers, $possible_answers[$pick];
545             splice @possible_answers, $pick, 1;
546             }
547            
548             @answers = shuffle(@answers);
549            
550             my @children = $self->{multi_answer_sizer}->GetChildren;
551            
552             for my $i ( 0..8 ) {
553             my $a = $children[$i]->GetWindow;
554             $a->SetLabel( $answers[$i] || '' );
555             }
556            
557             $self->show_sizer_contents( $self->{multi_answer_sizer} );
558             $el{answer}->Hide;
559            
560             EVT_KEY_DOWN( $self, \&evt_process_keyboard );
561             }
562            
563             sub update_user_data_db {
564             my ( $self, $item ) = @_;
565            
566             $self->{dbh}->do( "
567             REPLACE INTO $self->{set_table}
568             VALUES (?,?,?,?);
569             ", undef,
570             $item->{id}, $item->{certainty}, $item->{time_to_answer}, $item->{last_seen} );
571             }
572            
573             sub load_audiobank {
574             my ( $self, $audiobank ) = @_;
575            
576             eval "require $audiobank; $audiobank->import();";
577            
578             if ($@) {
579             $self->{audiobanks}->{$audiobank} = 'not_available';
580             return;
581             }
582            
583             my @content_list;
584            
585             eval "\@content_list = $audiobank->get_content_list;";
586             confess $@ if $@;
587            
588             my $dist = $audiobank;
589             $dist =~ s/::/-/g;
590             $self->{audiobank_paths}->{$audiobank} ||= dist_dir($dist);
591            
592             for my $file (@content_list) {
593             $self->{audiobanks}->{$audiobank}->{$file} = catfile( $self->{audiobank_paths}->{$audiobank}, $file );
594             }
595             }
596            
597            
598            
599             # GUI Setup Sub-Routines
600             ################################################################################
601            
602             sub load_gui {
603             my ($self) = @_;
604            
605             my $xr = Wx::XmlResource->new();
606             $xr->InitAllHandlers();
607             $xr->Load(catfile( dist_dir('Quiz-Flashcards'), 'gui.xrc' ));
608            
609             $el{frame} = Wx::Frame->new;
610             $xr->LoadFrame( $el{frame}, undef, 'frame' );
611            
612             $self->{main_sizer} = $el{frame}->GetSizer;
613            
614             my @children = $el{frame}->GetChildren;
615            
616             for my $child (@children) {
617             $el{ $child->GetName } = $child;
618             }
619            
620             $self->{bottom_sizer} = $el{start_next_button}->GetContainingSizer;
621             $self->{multi_answer_sizer} = $el{answer_0}->GetContainingSizer;
622            
623             $self->re_layout;
624             $el{frame}->Show(1);
625             }
626            
627             sub re_layout {
628             my ($self) = @_;
629            
630             $self->{main_sizer}->Layout;
631             $self->{main_sizer}->SetSizeHints( $el{frame} );
632             $el{frame}->Center;
633             }
634            
635             sub load_config {
636             my ($self) = @_;
637            
638             $self->{dbh} = setup_database();
639             $self->{c} = $self->{dbh}->selectall_hashref( "SELECT * FROM settings;", 'name' );
640            
641             for my $key ( keys %{ $self->{c} } ) {
642             $self->{c}->{$key} = $self->{c}->{$key}->{value};
643             }
644             }
645            
646             sub setup_database {
647             my $path = catfile( File::HomeDir->my_data, '.Quiz-Flashcards' );
648            
649             my $dbh = DBI->connect( "dbi:SQLite:dbname=$path", "", "" );
650            
651             $dbh->{HandleError} = sub { confess(shift) };
652            
653             $dbh->do( "
654             CREATE TABLE IF NOT EXISTS settings (
655             name TEXT PRIMARY KEY,
656             value TEXT
657             );
658             " );
659            
660             #$dbh->do("
661             # INSERT OR IGNORE INTO settings
662             # VALUES ( 'font_size_question', 'original' );
663             #");
664            
665             return $dbh;
666             }
667            
668             sub load_sets_into_selector {
669             my ($self) = @_;
670            
671             my @found = findallmod Quiz::Flashcards::Sets;
672             for my $module (@found) {
673             $module =~ s/Quiz::Flashcards::Sets:://;
674             $module =~ s/::/ -> /;
675             $el{set_selector}->Append($module);
676             }
677            
678             $self->re_layout;
679             }
680            
681             sub load_timers {
682             my ($self) = @_;
683            
684             $el{question_timer_id} = Wx::NewId;
685             $el{question_timer} = Wx::Timer->new( $self, $el{question_timer_id} );
686             $el{wrong_timer_id} = Wx::NewId;
687             $el{wrong_timer} = Wx::Timer->new( $self, $el{wrong_timer_id} );
688             }
689            
690             sub register_events {
691             my ($self) = @_;
692            
693             EVT_CHOICE( $self, $el{set_selector}, \&load_set );
694             EVT_BUTTON( $self, $el{set_status_toggle}, \&toggle_set_status );
695             EVT_BUTTON( $self, $el{start_next_button}, \&start_next_clicked );
696             EVT_TIMER( $self, $el{question_timer_id}, \&check_answer );
697             EVT_TIMER( $self, $el{wrong_timer_id}, \&enable_start_next_button );
698             EVT_TEXT_ENTER( $self, $el{answer}, \&check_answer );
699            
700             my @multi_answer_buttons = $self->{multi_answer_sizer}->GetChildren;
701             for my $button ( @multi_answer_buttons ) {
702             EVT_BUTTON( $self, $button->GetWindow, \&evt_multi_answer_button );
703             }
704             }
705            
706             sub load_waiting_animator {
707             my ($self) = @_;
708            
709             my $activity_anim_path = catfile( dist_dir('Quiz-Flashcards'), 'ajax-loader.gif' );
710             my $animation = Wx::Animation->new();
711             $animation->LoadFile( $activity_anim_path, wxANIMATION_TYPE_GIF );
712             $el{animator}->SetAnimation($animation);
713             $el{animator}->Play;
714             $el{animator}->Hide;
715            
716             $self->re_layout;
717             }
718            
719             sub adjust_fonts {
720             my ($self) = @_;
721            
722             my $question_font = $el{question}->GetFont;
723             $question_font->SetPointSize( $question_font->GetPointSize * 4 );
724             $el{question}->SetFont($question_font);
725             $el{answer}->Disable;
726             $el{correct_answer}->Hide;
727            
728             my $answer_font = $el{correct_answer}->GetFont;
729             $answer_font->SetPointSize( $answer_font->GetPointSize * 2 );
730             $el{correct_answer}->SetFont($answer_font);
731             }
732            
733             sub show_sizer_contents {
734             my ($self, $sizer) = @_;
735            
736             for my $child ( $sizer->GetChildren ) {
737             $child->GetWindow->Show;
738             }
739             }
740            
741             sub hide_sizer_contents {
742             my ($self, $sizer) = @_;
743            
744             for my $child ( $sizer->GetChildren ) {
745             $child->GetWindow->Hide;
746             }
747             }
748            
749             sub disable_sizer_contents {
750             my ($self, $sizer) = @_;
751            
752             for my $child ( $sizer->GetChildren ) {
753             $child->GetWindow->Disable;
754             }
755             }
756            
757             sub enable_sizer_contents {
758             my ($self, $sizer) = @_;
759            
760             for my $child ( $sizer->GetChildren ) {
761             $child->GetWindow->Enable;
762             }
763             }
764            
765             1; # End of Quiz::Flashcards