#!/usr/bin/perl
use strict;
use warnings;
binmode STDOUT, ':utf8';
use Parse::Win32Registry 0.51;
package KeyTreeCtrl;
use Wx qw(:everything);
use Wx::ArtProvider qw(:artid :clientid);
use Wx::Event qw(:everything);
use base qw(Wx::TreeCtrl);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxTR_DEFAULT_STYLE|wxBORDER_SUNKEN);
bless $self, $class;
my $imagelist = Wx::ImageList->new(16, 16, 1);
$imagelist->Add(Wx::ArtProvider::GetIcon(wxART_FOLDER, wxART_MENU, [16, 16]));
$imagelist->Add(Wx::ArtProvider::GetIcon(wxART_NORMAL_FILE, wxART_MENU, [16, 16]));
$self->AssignImageList($imagelist);
EVT_TREE_ITEM_EXPANDING($self, $self, \&OnTreeItemExpanding);
return $self;
}
sub Clear {
my ($self) = @_;
$self->DeleteAllItems;
}
sub SetRootKey {
my ($self, $root_key) = @_;
my $name = $root_key->get_name;
$name =~ s/\0/[NUL]/g;
$name =~ s/\n/[LF]/g;
$name =~ s/\r/[CR]/g;
my $root_item = $self->AddRoot($name, 0, -1);
$self->SetPlData($root_item, $root_key);
$self->AddChildren($root_item, $root_key);
}
sub AddChildren {
my ($self, $item, $key) = @_;
my @subkeys = $key->get_list_of_subkeys;
foreach my $subkey (@subkeys) {
my $name = $subkey->get_name;
$name =~ s/\0/[NUL]/g;
$name =~ s/\n/[LF]/g;
$name =~ s/\r/[CR]/g;
my $child_item = $self->AppendItem($item, $name, 0, -1);
$self->SetPlData($child_item, $subkey);
$self->SetItemHasChildren($child_item, 1);
}
return scalar @subkeys;
}
sub OnTreeItemExpanding {
my ($self, $event) = @_;
my $item = $event->GetItem;
my ($child_item, $cookie) = $self->GetFirstChild($item);
if ($child_item->IsOk) {
return; # already populated
}
my $key = $self->GetPlData($item);
if (!$self->AddChildren($item, $key)) {
$self->SetItemHasChildren($item, 0);
}
}
sub FindMatchingItem {
my ($self, $key_name, $item) = @_;
return if !$self->ItemHasChildren($item);
# Make any virtual children real before proceeding
my ($child_item, $cookie) = $self->GetFirstChild($item);
if (!$child_item->IsOk) { # children still virtual
my $key = $self->GetPlData($item);
if (!$self->AddChildren($item, $key)) {
$self->SetItemHasChildren($item, 0);
}
}
# Look through the children for a match
($child_item, $cookie) = $self->GetFirstChild($item);
while ($child_item->IsOk) {
my $key = $self->GetPlData($child_item);
if ($key_name eq $key->get_name) {
return $child_item; # found a match
}
($child_item, $cookie) = $self->GetNextChild($item, $cookie);
}
return; # no match
}
sub GoToSubkey {
my ($self, $subkey_path) = @_;
my $item = $self->GetRootItem;
my @key_names = split(/\\/, $subkey_path, -1);
# my @key_names = index($subkey_path, "\\") == -1
# ? ($subkey_path)
# : split(/\\/, $subkey_path, -1);
# If the first method is chosen, it is possible to go to the root key,
# but a first-level subkey with no name will be inaccessible.
# This is because an empty string will produce an empty array,
# causing the following while loop to be skipped,
# leaving $item set to the root.
# If the second method is chosen, it is possible to go to a first-level
# subkey with no name, but the root key will be inaccessible.
# This is because an array with at least one string in it is produced,
# causing the following while loop to be entered, and either
# the first-level subkey will be found and $item will be set to it,
# or it will not be found, and the subroutine will return
# without going to any item.
while (@key_names) {
my $key_name = shift @key_names;
$item = $self->FindMatchingItem($key_name, $item);
if (!defined $item) {
return; # no match found
}
}
# match found, in $item
$self->EnsureVisible($item);
$self->SelectItem($item);
}
sub GetSelectedKey {
my ($self) = @_;
my $item = $self->GetSelection;
if ($item->IsOk) {
my $key = $self->GetPlData($item);
return $key;
}
return;
}
package ValueListCtrl;
use Wx qw(:everything);
use Wx::ArtProvider qw(:artid :clientid);
use base qw(Wx::ListCtrl);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, wxDefaultSize, wxLC_REPORT|wxLC_SINGLE_SEL|wxBORDER_SUNKEN);
bless $self, $class;
$self->InsertColumn(0, "Name", wxLIST_FORMAT_LEFT);
$self->InsertColumn(1, "Type", wxLIST_FORMAT_LEFT);
$self->InsertColumn(2, "Data", wxLIST_FORMAT_LEFT);
$self->SetColumnWidth(0, 150);
$self->SetColumnWidth(1, 100);
$self->SetColumnWidth(2, 150);
my $imagelist = Wx::ImageList->new(16, 16, 1);
$imagelist->Add(Wx::ArtProvider::GetIcon(wxART_NORMAL_FILE, wxART_MENU, [16, 16]));
$self->AssignImageList($imagelist, wxIMAGE_LIST_SMALL);
return $self;
}
sub SetKey {
my ($self, $key) = @_;
return unless $key->can('get_list_of_values');
my @values = $key->get_list_of_values;
$self->DeleteAllItems;
my $index = 0;
foreach my $value (@values) {
my $name = $value->get_name;
$name = "(Default)" if $name eq '';
$name =~ s/\0/[NUL]/g;
$name =~ s/\n/[LF]/g;
$name =~ s/\r/[CR]/g;
my $type = $value->get_type_as_string;
my $data = substr($value->get_data_as_string, 0, 200);
$data =~ s/\0/[NUL]/g;
$data =~ s/\n/[LF]/g;
$data =~ s/\r/[CR]/g;
$index = $self->InsertImageStringItem($index+1, $name, 0);
$self->SetItem($index, 1, $type);
$self->SetItem($index, 2, $data);
}
$self->{_key} = $key;
$self->{_values} = \@values;
}
sub GetValue {
my ($self, $index) = @_;
return $self->{_values}[$index];
}
sub Clear {
my ($self) = @_;
$self->DeleteAllItems;
$self->{_key} = undef;
$self->{_values} = undef;
}
sub GoToValue {
my ($self, $value_name) = @_;
for (my $index = 0; $index < @{$self->{_values}}; $index++) {
if ($value_name eq $self->{_values}[$index]->get_name) {
$self->EnsureVisible($index);
$self->SetItemState($index, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
}
}
}
package ViewFrame;
use File::Basename;
use FindBin;
use Parse::Win32Registry qw(hexdump);
use Wx qw(:everything);
use Wx::DND; # required for copying to clipboard
use Wx::Event qw(:everything);
use base qw(Wx::Frame);
use constant ID_DUMP_KEYS => Wx::NewId;
use constant ID_FIND_NEXT => Wx::NewId;
use constant ID_TIMELINE => Wx::NewId;
use constant ID_SELECT_FONT => Wx::NewId;
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, "Registry Viewer", wxDefaultPosition, [600, 400]);
bless $self, $class;
$self->SetMinSize([600, 400]);
my $menu1 = Wx::Menu->new;
$menu1->Append(wxID_OPEN, "&Open...\tCtrl+O");
$menu1->Append(wxID_CLOSE, "&Close\tCtrl+W");
$menu1->AppendSeparator;
$menu1->Append(wxID_EXIT, "E&xit\tAlt+F4");
my $menu2 = Wx::Menu->new;
$menu2->Append(wxID_COPY, "&Copy Key Path\tCtrl+C");
my $menu3 = Wx::Menu->new;
$menu3->Append(wxID_FIND, "&Find...\tCtrl+F");
$menu3->Append(ID_FIND_NEXT, "Find &Next\tF3");
$menu3->AppendSeparator;
$menu3->Append(ID_TIMELINE, "Show &Timeline...");
my $menu4 = Wx::Menu->new;
$menu4->Append(ID_SELECT_FONT, "Select &Font...");
my $menu5 = Wx::Menu->new;
$menu5->Append(wxID_ABOUT, "&About...");
my $menubar = Wx::MenuBar->new;
$menubar->Append($menu1, "&File");
$menubar->Append($menu2, "&Edit");
$menubar->Append($menu3, "&Search");
$menubar->Append($menu4, "&View");
$menubar->Append($menu5, "&Help");
$self->SetMenuBar($menubar);
my $statusbar = Wx::StatusBar->new($self, -1);
$self->SetStatusBar($statusbar);
EVT_MENU($self, wxID_OPEN, \&OnOpenFile);
EVT_MENU($self, wxID_CLOSE, \&OnCloseFile);
EVT_MENU($self, wxID_EXIT, \&OnQuit);
EVT_MENU($self, wxID_COPY, \&OnCopy);
EVT_MENU($self, wxID_FIND, \&OnFind);
EVT_MENU($self, ID_FIND_NEXT, \&FindNext);
EVT_MENU($self, ID_TIMELINE, \&ShowTimeline);
EVT_MENU($self, wxID_ABOUT, \&OnAbout);
EVT_MENU($self, ID_SELECT_FONT, \&OnSelectFont);
my $hsplitter = Wx::SplitterWindow->new($self, -1, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER);
my $tree = KeyTreeCtrl->new($hsplitter);
my $vsplitter = Wx::SplitterWindow->new($hsplitter, -1, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER);
$hsplitter->SplitVertically($tree, $vsplitter);
$hsplitter->SetMinimumPaneSize(10);
my $list = ValueListCtrl->new($vsplitter);
my $text = Wx::TextCtrl->new($vsplitter, -1, '', wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_DONTWRAP|wxTE_READONLY);
# Set a monospaced font
$text->SetFont(Wx::Font->new(10, wxMODERN, wxNORMAL, wxNORMAL));
$vsplitter->SplitHorizontally($list, $text);
$vsplitter->SetMinimumPaneSize(10);
$self->{_tree} = $tree;
$self->{_list} = $list;
$self->{_text} = $text;
$self->{_statusbar} = $statusbar;
EVT_SPLITTER_DCLICK($self, $hsplitter, \&OnSplitterDClick);
EVT_SPLITTER_DCLICK($self, $vsplitter, \&OnSplitterDClick);
EVT_TREE_SEL_CHANGED($self, $tree, \&OnKeyTreeSelChanged);
EVT_LIST_ITEM_SELECTED($self, $list, \&OnValueListItemSelected);
$self->SetIcon(Wx::GetWxPerlIcon());
my $accelerators = Wx::AcceleratorTable->new(
[wxACCEL_CTRL, ord('Q'), wxID_EXIT],
);
$self->SetAcceleratorTable($accelerators);
my $filename = shift @ARGV;
if (defined $filename) {
$self->LoadFile($filename);
}
return $self;
}
sub OnSelectFont {
my ($self, $event) = @_;
my $text = $self->{_text};
my $font = $text->GetFont;
$font = Wx::GetFontFromUser($self, $font);
if ($font->IsOk) {
$text->SetFont($font);
}
}
sub OnSplitterDClick {
my ($self, $event) = @_;
$event->Veto;
}
sub ShowTimeline {
my ($self, $event) = @_;
return if !defined $self->{_root_key};
my $dialog = $self->{_timeline_dialog};
if (!defined $dialog) {
$dialog = $self->{_timeline_dialog} = TimelineDialog->new($self);
# OnKeyListItemSelected
EVT_LIST_ITEM_SELECTED($self, $dialog->{_list2}, sub {
my ($self, $event) = @_;
my $index = $event->GetIndex;
my $key = $dialog->{_list2}->GetKey($index);
if (defined $key) {
my $subkey_path = (split(/\\/, $key->get_path, 2))[1];
$self->{_tree}->GoToSubkey($subkey_path);
}
});
# OnKeyListItemActivated
EVT_LIST_ITEM_ACTIVATED($self, $dialog->{_list2}, sub {
my ($self, $event) = @_;
$self->Raise;
});
my $font = $self->{_tree}->GetFont;
$dialog->{_list1}->SetFont($font);
$dialog->{_list2}->SetFont($font);
}
if (!defined $self->{_keys_by_time}) {
$self->BuildTimeline;
return if !defined $self->{_keys_by_time}; # build was cancelled
$dialog->SetTimeline($self->{_keys_by_time});
}
if (scalar keys %{$self->{_keys_by_time}} == 0) {
my $dialog = Wx::MessageDialog->new($self,
'No keys have timestamps!', 'Timeline', wxICON_ERROR|wxOK);
$dialog->ShowModal;
$dialog->Destroy;
return;
}
$dialog->Show;
$dialog->Raise;
$dialog->{_list1}->SetFocus;
}
sub BuildTimeline {
my ($self) = @_;
return if defined $self->{_keys_by_time};
my $root_key = $self->{_root_key};
return if !defined $root_key;
my $subtree_iter = $root_key->get_subtree_iterator;
my %keys_by_time = ();
my $max = 0;
my $progress_dialog = Wx::ProgressDialog->new('Building Timeline',
'Ordering registry keys...', $max, $self,
wxPD_CAN_ABORT|wxPD_AUTO_HIDE);
$progress_dialog->Update;
while (my $key = $subtree_iter->get_next) {
my $time = $key->get_timestamp;
push @{$keys_by_time{$time}}, $key if defined $time;
if (!$progress_dialog->Update) {
# Cancelled!
$progress_dialog->Destroy;
return;
}
}
$self->{_keys_by_time} = \%keys_by_time;
$progress_dialog->Destroy;
}
sub OnCopy {
my ($self, $event) = @_;
my $key = $self->{_tree}->GetSelectedKey;
my $clip = '';
if (defined $key) {
$clip = $key->get_path;
}
wxTheClipboard->Open;
wxTheClipboard->SetData(Wx::TextDataObject->new($clip));
wxTheClipboard->Close;
}
sub OnKeyTreeSelChanged {
my ($self, $event) = @_;
my $item = $event->GetItem;
my $key = $self->{_tree}->GetPlData($item);
$self->{_list}->SetKey($key);
return if !$key->can('get_list_of_values');
my $details = '';
if (defined $key->get_timestamp) {
$details .= "Timestamp: " . $key->get_timestamp_as_string . "\n";
}
my $class_name = $key->get_class_name;
if (defined $class_name) {
$class_name =~ s/\0/[NUL]/g;
$class_name =~ s/\n/[LF]/g;
$class_name =~ s/\r/[CR]/g;
$details .= "Class Name: $class_name\n";
}
my $security = $key->get_security;
if (defined $security) {
my $sd = $security->get_security_descriptor;
$details .= $sd->as_stanza;
}
$self->{_text}->ChangeValue($details);
my $key_str = $key->as_string;
$key_str =~ s/\0/[NUL]/g;
$key_str =~ s/\n/[LF]/g;
$key_str =~ s/\r/[CR]/g;
$self->{_statusbar}->SetStatusText($key_str);
}
sub OnValueListItemSelected {
my ($self, $event) = @_;
my $value = $self->{_list}->GetValue($event->GetIndex);
my $details = hexdump($value->get_raw_data);
$self->{_text}->ChangeValue($details);
}
sub OnAbout {
my ($self, $event) = @_;
my $info = Wx::AboutDialogInfo->new;
$info->SetName($FindBin::Script);
$info->SetVersion($Parse::Win32Registry::VERSION);
$info->SetCopyright('Copyright (c) 2010-2012 James Macfarlane');
$info->SetDescription('wxWidgets Registry Viewer for the Parse::Win32Registry module');
Wx::AboutBox($info);
}
sub FindNext {
my ($self) = @_;
my $find_param = $self->{_find_param};
my $find_iter = $self->{_find_iter};
my $search_keys = $self->{_search_keys};
my $search_values = $self->{_search_values};
return if !defined $find_param || $find_param eq '';
return if !defined $find_iter;
my $start = time;
my $max = 0;
my $progress_dialog;
my $iter_finished = 1;
while (my ($key, $value) = $find_iter->get_next) {
my $key_name = $key->get_name;
my $key_path = $key->get_path;
# strip root key name from path to get subkey path
my $subkey_path = (split(/\\/, $key_path, 2))[1];
if (defined $value) { # check value for match
if ($search_values) {
my $value_name = $value->get_name;
if (index(lc $value_name, lc $find_param) >= 0) {
$self->{_tree}->GoToSubkey($subkey_path);
$self->{_list}->GoToValue($value_name);
$self->{_list}->SetFocus;
$self->SetFocus;
$iter_finished = 0;
last;
}
}
}
elsif ($search_keys) { # check key for match
if (index(lc $key_name, lc $find_param) >= 0) {
$self->{_tree}->GoToSubkey($subkey_path);
$self->{_tree}->SetFocus;
$self->SetFocus;
$iter_finished = 0;
last;
}
}
if (defined $progress_dialog) {
if (!$progress_dialog->Update) {
# Cancelled!
$iter_finished = 0;
last;
}
}
else {
# display progress dialog if search is slow
if (time - $start >= 1) {
$progress_dialog = Wx::ProgressDialog->new('Find',
'Searching registry...', $max, $self,
wxPD_CAN_ABORT|wxPD_AUTO_HIDE);
}
}
}
if (defined $progress_dialog) {
$progress_dialog->Destroy;
}
if ($iter_finished) {
my $dialog = Wx::MessageDialog->new($self,
'Finished searching', 'Find', wxICON_EXCLAMATION|wxOK);
$dialog->ShowModal;
$dialog->Destroy;
}
}
sub OnFind {
my ($self, $event) = @_;
my $root_key = $self->{_root_key};
return if !defined $root_key;
my $dialog = FindDialog->new($self);
$dialog->SetText($self->{_find_param});
$dialog->SetSearchKeys($self->{_search_keys});
$dialog->SetSearchValues($self->{_search_values});
$dialog->SetSearchSelected($self->{_search_selected});
if ($dialog->ShowModal == wxID_OK) {
$self->{_find_param} = $dialog->GetText;
$self->{_search_keys} = $dialog->GetSearchKeys;
$self->{_search_values} = $dialog->GetSearchValues;
if (!$self->{_search_keys} && !$self->{_search_values}) {
$self->{_search_keys} = $self->{_search_values} = 1;
}
my $selected_key = $self->{_tree}->GetSelectedKey;
my $search_selected = $self->{_search_selected}
= $dialog->GetSearchSelected;
$self->{_find_iter} = $search_selected
? $selected_key->get_subtree_iterator
: $root_key->get_subtree_iterator;
$self->FindNext;
}
$dialog->Destroy;
}
sub LoadFile {
my ($self, $filename) = @_;
if (!-r $filename) {
my $dialog = Wx::MessageDialog->new($self,
"'$filename' cannot be read", 'Error', wxICON_ERROR|wxOK);
$dialog->ShowModal;
$dialog->Destroy;
return
}
my $basename = basename($filename);
my $registry = Parse::Win32Registry->new($filename);
if (!defined $registry) {
my $dialog = Wx::MessageDialog->new($self,
"'$basename' is not a registry file", 'Error', wxICON_ERROR|wxOK);
$dialog->ShowModal;
$dialog->Destroy;
return
}
my $root_key = $registry->get_root_key;
if (!defined $registry) {
my $dialog = Wx::MessageDialog->new($self,
"'$basename' has no root key", 'Error', wxICON_ERROR|wxOK);
$dialog->ShowModal;
$dialog->Destroy;
return;
}
# clear
$self->OnCloseFile;
# set up
$self->{_root_key} = $root_key;
$self->{_tree}->SetRootKey($root_key);
$self->{_tree}->SetFocus;
$self->SetTitle("$basename - Registry Viewer");
}
sub OnOpenFile {
my ($self, $event) = @_;
my $dialog = Wx::FileDialog->new($self, 'Select Registry File', $self->{_directory} || '');
if ($dialog->ShowModal != wxID_OK) {
return;
}
my $filename = $dialog->GetPath;
$self->{_directory} = $dialog->GetDirectory;
$self->LoadFile($filename);
}
sub OnCloseFile {
my ($self, $event) = @_;
$self->{_tree}->Clear;
$self->{_list}->Clear;
$self->{_text}->Clear;
$self->{_statusbar}->SetStatusText('');
$self->{_root_key} = undef;
$self->{_find_iter} = undef;
$self->{_keys_by_time} = undef;
$self->SetTitle("Registry Viewer");
if (defined $self->{_timeline_dialog}) {
$self->{_timeline_dialog}->SetTimeline({});
$self->{_timeline_dialog}->Hide;
}
}
sub OnQuit {
my ($self) = @_;
$self->Close;
}
package FindDialog;
use Wx qw(:everything);
use Wx::Event qw(:everything);
use base qw(Wx::Dialog);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, "Find", wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE);
bless $self, $class;
my $static = Wx::StaticText->new($self, -1, 'Enter text to &search for:');
my $text = Wx::TextCtrl->new($self, -1, '');
my $check1 = Wx::CheckBox->new($self, -1, 'Search &keys');
my $check2 = Wx::CheckBox->new($self, -1, 'Search &values');
my $radio = Wx::RadioBox->new($self, -1, 'Start searching', wxDefaultPosition, wxDefaultSize, ['from root key', 'from current key'], 1);
my $sizer = Wx::BoxSizer->new(wxVERTICAL);
$sizer->Add($static, 0, wxEXPAND|wxALL, 5);
$sizer->Add($text, 0, wxEXPAND|wxALL, 5);
$sizer->Add($check1, 0, wxALL, 5);
$sizer->Add($check2, 0, wxALL, 5);
$sizer->Add($radio, 0, wxALL, 5);
my $button_sizer = $self->CreateSeparatedButtonSizer(wxOK|wxCANCEL);
$sizer->Add($button_sizer, 0, wxEXPAND|wxALL, 5);
$self->SetSizer($sizer);
$self->{_text} = $text;
$self->{_check1} = $check1;
$self->{_check2} = $check2;
$self->{_radio} = $radio;
$self->Fit; # resize dialog to best fit child windows
$self->{_text}->SetFocus;
$self->SetFocus;
EVT_CHECKBOX($self, $check1, sub {
if (!$check1->GetValue && !$check2->GetValue) {
$check2->SetValue(1);
}
});
EVT_CHECKBOX($self, $check2, sub {
if (!$check1->GetValue && !$check2->GetValue) {
$check1->SetValue(1);
}
});
return $self;
}
sub GetSearchKeys {
my ($self) = @_;
return $self->{_check1}->GetValue;
}
sub GetSearchValues {
my ($self) = @_;
return $self->{_check2}->GetValue;
}
sub GetText {
my ($self) = @_;
return $self->{_text}->GetValue;
}
sub GetSearchSelected {
my ($self) = @_;
return $self->{_radio}->GetSelection;
}
sub SetSearchKeys {
my ($self, $state) = @_;
$state = 1 if !defined $state;
$self->{_check1}->SetValue($state);
}
sub SetSearchValues {
my ($self, $state) = @_;
$state = 1 if !defined $state;
$self->{_check2}->SetValue($state);
}
sub SetText {
my ($self, $value) = @_;
$value = '' if !defined $value;
$self->{_text}->ChangeValue($value);
$self->{_text}->SetSelection(-1, -1);
}
sub SetSearchSelected {
my ($self, $n) = @_;
$n = 0 if !defined $n;
$self->{_radio}->SetSelection($n);
}
package TimeListCtrl;
use Parse::Win32Registry qw(iso8601);
use Wx qw(:everything);
use base qw(Wx::ListCtrl);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, [200, -1], wxLC_REPORT|wxLC_SINGLE_SEL|wxLC_VIRTUAL|wxBORDER_SUNKEN);
bless $self, $class;
$self->InsertColumn(0, "Time");
$self->InsertColumn(1, "Count");
$self->SetColumnWidth(0, 200);
$self->{_times} = [];
$self->{_key_counts} = [];
return $self;
}
sub OnGetItemText {
my ($self, $index, $column) = @_;
if ($column == 0) {
return iso8601($self->{_times}[$index]);
}
elsif ($column == 1) {
return $self->{_key_counts}[$index];
}
else {
return "?";
}
}
sub SetTimes {
my ($self, $times, $key_counts) = @_;
$self->{_times} = $times;
$self->{_key_counts} = $key_counts;
$self->SetItemCount(scalar @$times);
$self->Refresh;
$self->SetItemState(0, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
}
sub GetTime {
my ($self, $index) = @_;
return $self->{_times}[$index];
}
package KeyListCtrl;
use Wx qw(:everything);
use Wx::ArtProvider qw(:artid :clientid);
use base qw(Wx::ListCtrl);
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, wxDefaultPosition, [200, -1], wxLC_REPORT|wxLC_SINGLE_SEL|wxLC_VIRTUAL|wxBORDER_SUNKEN);
bless $self, $class;
my $imagelist = Wx::ImageList->new(16, 16, 1);
$imagelist->Add(Wx::ArtProvider::GetIcon(wxART_FOLDER, wxART_MENU, [16, 16]));
$self->AssignImageList($imagelist, wxIMAGE_LIST_SMALL);
$self->InsertColumn(0, "Key");
$self->SetColumnWidth(0, 280);
return $self;
}
sub OnGetItemText {
my ($self, $index, $column) = @_;
my $key = $self->{_keys}[$index];
return if !defined $key;
if ($column == 0) {
my $key_path = $key->get_path;
$key_path =~ s/\0/[NUL]/g;
$key_path =~ s/\n/[LF]/g;
$key_path =~ s/\r/[CR]/g;
return $key_path;
}
else {
return "?";
}
}
sub OnGetItemImage {
my ($self, $index) = @_;
return 0;
}
sub SetKeys {
my ($self, $keys) = @_;
$self->{_keys} = $keys;
$self->SetItemCount(scalar @$keys);
$self->Refresh;
$self->SetItemState(0, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED);
}
sub GetKey {
my ($self, $index) = @_;
return $self->{_keys}[$index];
}
package TimelineDialog;
use Wx qw(:everything);
use Wx::Event qw(:everything);
use base qw(Wx::Frame);
use constant ID_CLOSE_DIALOG => Wx::NewId;
sub new {
my ($class, $parent) = @_;
my $self = $class->SUPER::new($parent, -1, "Timeline", wxDefaultPosition, [600, 300]);
bless $self, $class;
$self->SetMinSize([600, 300]);
my $hsplitter = Wx::SplitterWindow->new($self, -1, wxDefaultPosition, wxDefaultSize, wxSP_NOBORDER);
my $list1 = TimeListCtrl->new($hsplitter);
my $list2 = KeyListCtrl->new($hsplitter);
$hsplitter->SplitVertically($list1, $list2);
$hsplitter->SetMinimumPaneSize(10);
$self->{_list1} = $list1;
$self->{_list2} = $list2;
my $accelerators = Wx::AcceleratorTable->new(
[0, WXK_ESCAPE, ID_CLOSE_DIALOG],
[wxACCEL_CTRL, ord('W'), ID_CLOSE_DIALOG],
);
$self->SetAcceleratorTable($accelerators);
EVT_MENU($self, ID_CLOSE_DIALOG, \&OnClose);
EVT_SPLITTER_DCLICK($self, $hsplitter, \&OnSplitterDClick);
EVT_LIST_ITEM_SELECTED($self, $list1, \&OnTimeListItemSelected);
EVT_CLOSE($self, \&OnClose);
$self->SetIcon(Wx::GetWxPerlIcon());
return $self;
}
sub OnSplitterDClick {
my ($self, $event) = @_;
$event->Veto;
}
sub OnTimeListItemSelected {
my ($self, $event) = @_;
my $index = $event->GetIndex;
my $time = $self->{_list1}->GetTime($index);
$self->{_list2}->SetKeys($self->{_keys_by_time}{$time});
}
sub OnKeyListItemActivated {
my ($self, $event) = @_;
$self->Close;
}
sub SetTimeline {
my ($self, $keys_by_time) = @_;
my @times = sort keys %$keys_by_time;
my @key_counts = map { scalar @{$keys_by_time->{$_}} } @times;
my $list1 = $self->{_list1};
$list1->SetTimes(\@times, \@key_counts);
my $list2 = $self->{_list2};
$list2->SetKeys([]);
$self->{_times} = \@times;
$self->{_keys_by_time} = $keys_by_time;
}
sub OnClose {
my ($self, $event) = @_;
$self->Hide;
}
package ViewApp;
use Wx qw(:everything);
use base qw(Wx::App);
sub OnInit {
my ($self) = @_;
my $frame = ViewFrame->new(undef);
$frame->Show;
return 1;
}
package main;
my $app = ViewApp->new;
$app->MainLoop;