package dtRdr::GUI::Wx::NoteViewer; $VERSION = eval{require version}?version::qv($_):$_ for(0.10.1); use warnings; use strict; use Carp; use dtRdr::Note; use dtRdr::Logger; use dtRdr::HTMLWidget; use Wx; use base 'Wx::Panel'; use Wx::Event qw( EVT_BUTTON EVT_SPLITTER_SASH_POS_CHANGED EVT_SPLITTER_SASH_POS_CHANGING EVT_SPLITTER_DOUBLECLICKED ); use WxPerl::ShortCuts; use Class::Accessor::Classy; ro qw( frame bv_manager htmlwidget window state title_bar bt_goto bt_edit bt_delete bt_close ); rw qw( notebar_changing ); rs thread => \ (my $set_thread); no Class::Accessor::Classy; =head1 NAME dtRdr::GUI::Wx::NoteViewer - a special Wx::Panel =head1 SYNOPSIS =cut =head2 new my $nv = dtRdr::GUI::Wx::NoteViewer->new($parent, @args); =cut sub new { my $class = shift; my ($parent, @args) = @_; my $self = $class->SUPER::new($parent, @args); return($self); } # end subroutine new definition ######################################################################## =head2 init $nv->init($frame); =cut sub init { my $self = shift; my ($frame) = @_; { # copy-in some frame stuff $self->{frame} = $frame; $self->{bv_manager} = $frame->bv_manager; $self->{window} = $frame->right_window; $self->{state} = $frame->state; } $self->__create_children; $self->__do_properties; $self->__do_layout; # TODO make url_handler be something besides $self $self->htmlwidget->init($self, url_handler => $self); $self->SetMinSize(Wx::Size->new(-1, 0)); { # connect the buttons my @button_map = ( ['goto' => 'goto_note'], #['edit' => 'edit_note'], #['delete' => 'delete_note'], ['close' => sub { $self->notebar_toggle if($self->state->notebar_open); } ], ); foreach my $row (@button_map) { my ($action, $sub) = @$row; $sub = eval("sub {\$self->$sub}") unless(ref($sub) eq 'CODE'); my $bt_name = 'bt_' . $action; EVT_BUTTON($self, $self->$bt_name, sub { WARN("$action button"); $sub->(); }); } } # end buttons EVT_SPLITTER_SASH_POS_CHANGING($self->window, $self->window, sub { $self->set_notebar_changing(1); $_[1]->Skip } ); EVT_SPLITTER_SASH_POS_CHANGED($self->window, $self->window, sub { $self->notebar_changed($_[1]) } ); EVT_SPLITTER_DOUBLECLICKED($self->window, $self->window, sub { $self->notebar_toggle() } ); } # end subroutine init definition ######################################################################## =head2 load_url Acts as a url_handler for the viewer's htmlwidget. $self->load_url($url, $killit); =cut sub load_url { my $self = shift; my ($url, $kill) = @_; #if($^O eq 'MSWin32') { # now, this is just getting silly $kill and $kill->(); # because we're loading it in another pane #} use URI; my $uri = URI->new($url); RL('#links')->debug("nv pondering $url"); if(($uri->scheme || '') eq 'dr') { RL('#links')->debug("nv dr link: '$url'"); my $auth = $uri->authority; if($auth eq 'NOTEVIEW') { $kill and $kill->(); my @parts = split(/\/+/, $uri->path); shift(@parts); # drop leading null # maybe no biggie if there's too many (@parts == 2) or warn "scary path (@parts) '$url', probably broken..."; my ($action, $id) = @parts; WARN "do $action to $id"; my $method = $action . '_note'; $self->can($method) or die "no way to handle action '$action'"; $self->$method($id); return; } } $self->bv_manager->book_view->load_url(@_); } # end subroutine load_url definition ######################################################################## =head2 __create_children $self->__create_children; =cut sub __create_children { my $self = shift; $self->{title_bar} = Wx::StaticText->new($self, -1, "Title goes here", DefPS); $self->{"bt_$_"} = $self->aBitmapButton( dtRdr::GUI::Wx::Utils->Bitmap("nv_button_$_") ) for(qw(goto close)); $self->{htmlwidget} = dtRdr::HTMLWidget->new([$self, -1, DefPS]); } # end subroutine __create_children definition ######################################################################## sub aBitmapButton { # TODO WxPerl::Spawn or something my $self = shift; Wx::BitmapButton->new($self, -1, @_); } =head2 __do_layout $self->__do_layout; =cut sub __do_layout { my $self = shift; my $s = $self->{sizer} = Wx::BoxSizer->new(wV); my $gs = $self->{gridsizer} = Wx::FlexGridSizer->new(1, 3, 0, 0); $gs->Add($self->title_bar, 0, Exp|Ams, 0); $gs->Add($self->bt_goto, 0, Ams, 0); #$gs->Add($self->bt_edit, 0, Ams, 0); #$gs->Add($self->bt_delete, 0, Ams, 0); $gs->Add($self->bt_close, 0, Ams, 0); $gs->AddGrowableCol(0); $s->Add($gs, 0, WX"RIGHT|EXPAND|ALIGN_RIGHT|ADJUST_MINSIZE", 0); $s->Add($self->htmlwidget, 1, Exp, 0); $self->SetAutoLayout(1); $self->SetSizer($s); $s->Fit($self); $s->SetSizeHints($self); } # end subroutine __do_layout definition ######################################################################## =head2 __do_properties $self->__do_properties; =cut sub __do_properties { my $self = shift; $self->title_bar->SetFont(Wx::Font->new(12, Def, WX"NORMAL", WX"BOLD", 0, "")); $self->bt_goto->SetToolTipString("goto"); #$self->bt_edit->SetToolTipString("edit"); #$self->bt_delete->SetToolTipString("delete"); $self->bt_close->SetToolTipString("close"); } # end subroutine __do_properties definition ######################################################################## =head2 setup $nv->setup; =cut sub setup { my $self = shift; $self->no_note; if(0) { my $greeting = qq() . "no note" . ''; $self->htmlwidget->SetPage($greeting); } { # set the size my $win = $self->window; my ($x, $y) = $win->GetSizeWH; 0 and WARN("window size $x, $y"); # might Hide if the button slivers are annoying, but then we'll have # to manually show it #$self->Show(0); $win->SetSashPosition($y); # NOTE a bug on the widget? } } # end subroutine setup definition ######################################################################## =head2 _enable_buttons $self->_enable_buttons; =cut sub _enable_buttons { my $self = shift; my ($bool) = @_; $bool = 1 unless(@_); defined($bool) or croak("invalid"); foreach my $name (qw( goto )) { my $attribute = 'bt_' . $name; $self->$attribute->Enable($bool); } } # end subroutine _enable_buttons definition ######################################################################## =head2 _disable_buttons $self->_disable_buttons; =cut sub _disable_buttons { my $self = shift; $self->_enable_buttons(0); } # end subroutine _disable_buttons definition ######################################################################## =head2 notebar_changed $self->notebar_changed($event); =cut sub notebar_changed { my $self = shift; my ($event) = @_; RL('#notebar')->debug("'notebar_changed' fired"); # this fires on a resize -- really bad, so we'll just track and skip $self->notebar_changing or return; $self->set_notebar_changing(0); # TODO there's one more case here where the window gets shrunk such # that our size is forced to be reduced. When it is shrunk or # expanded (ala F7) this event fires once when it is done (~because # we're on the bottom here and gravity says so.) At the moment, we're # not remembering the position that results from shrinkage (which is # good) but if the user re-enlarges the window and then fires the # toggle, it will change from the forced size to the remembered size, # which could be unsettling. We should probably see if we have enough # room to reasonably expand back to our remembered size and then do # it. my $state = $self->state; # ok, this only fires on manual drags and not SetSashPosition() ? my $new_pos = $event->GetSashPosition; my $height = ($self->window->GetSizeWH)[1]; my $nb_size = $height - $new_pos; RL('#notebar')->debug("pos changed to $new_pos ($nb_size)"); if($nb_size < 60) { # meh, call it a draggy-toggle and DWIM if($state->notebar_open) { # you meant close, right? $state->set_notebar_open(0); $self->window->SetSashPosition($height); } else { # magic open # (un?)fortunately, this means the doubleclick is not needed from # the closed position. Is that inconsistent? $state->set_notebar_open(1); $self->window->SetSashPosition($height - $state->notebar_position); } } else { # remember the new position (from the bottom) $state->set_notebar_position($nb_size); $state->set_notebar_open(1); # just in case } } # end subroutine notebar_changed definition ######################################################################## =head2 notebar_toggle $self->notebar_toggle($event); =cut sub notebar_toggle { my $self = shift; my ($event) = @_; my $state = $self->state; my $pos = $self->window->GetSashPosition(); my $height = ($self->window->GetSizeWH)[1]; # TODO focus whichever tab is on top RL('#notebar')->debug("window toggle: $pos"); # NOTE mac gets silly about this if SashPosition is less than 3 my $open = $state->notebar_open; $self->window->SetSashPosition( $height - ($open ? 0 : $state->notebar_position) ); $state->set_notebar_open(! $open); } # end subroutine notebar_toggle definition ######################################################################## =head2 be_open $nv->be_open; =cut sub be_open { my $self = shift; $self->notebar_toggle unless($self->state->notebar_open); } # end subroutine be_open definition ######################################################################## =head2 be_closed $nv->be_closed; =cut sub be_closed { my $self = shift; $self->notebar_toggle if($self->state->notebar_open); } # end subroutine be_closed definition ######################################################################## =head1 Note Manipulation The viewer has a concept of a "current" note, which is updated by the BVManager and the C method. =head2 thread_id Return the ID of the current thread. Returns undef if there is not one. my $id = $nv->thread_id; =cut sub thread_id { my $self = shift; my $thread = $self->thread; defined($thread) or return(); return($thread->id); } # end subroutine thread_id definition ######################################################################## =head2 no_note Quit showing whatever note you were showing for whatever reason? $nv->no_note; =cut # TODO rename as clear() ? sub no_note { my $self = shift; # disable the control buttons $self->_disable_buttons; $self->title_bar->SetLabel('- no note -'); $self->htmlwidget->SetPage(''); if(my $note = delete($self->{note})) { $note->DESTROY; } $self->$set_thread(undef); # TODO anything else? $self->be_closed; } # end subroutine no_note definition ######################################################################## =head2 goto_note $self->goto_note; =cut sub goto_note { my $self = shift; $self->bv_manager->book_view->jump_to($self->thread->note); } # end subroutine goto_note definition ######################################################################## =head2 edit_note $self->edit_note($id); =cut sub edit_note { my $self = shift; my ($id) = @_; # need to get the note from ... my $got = $self->have($id) or die "I don't have that note ($id)"; if($got->is_fake) { die "can't edit the dummy notes"; } $self->bv_manager->edit_note($got); } # end subroutine edit_note definition ######################################################################## # XXX grr, this is inefficient, but I don't want to deal with staleness # with the current architectural state of things sub have { my $self = shift; my ($id) = @_; my $thread = $self->thread or die "no thread here?"; my %have = $thread->rmap(sub {$_->id => $_}); my $got = $have{$id} or return(); return($got->note); } =head2 delete_note $self->delete_note($id); =cut sub delete_note { my $self = shift; my ($id) = @_; defined($id) or die "wrong api"; # should just bv_manager->**_note() ? WARN "lookup"; my $note = $self->have($id) or die "I don't have that note"; WARN "do it"; if($note->is_fake) { die "can't delete the dummy notes"; } # TODO "are you sure" dialog $note->book->delete_note($note); } # end subroutine delete_note definition ######################################################################## =head2 reply_note $self->reply_note($id); =cut sub reply_note { my $self = shift; my ($id) = @_; my $got = $self->have($id) or die "I don't have that note ($id)"; my $title = $got->title; $title = "Re: $title" unless($title =~ m/^re: */i); my $note = dtRdr::Note->create( node => $got->node, range => $got, title => $title, references => [$got->id, $got->references], ); $self->bv_manager->create_note($note, ($got->public ? (public => 1) : ()) ); } # end subroutine reply_note definition ######################################################################## =head2 show_note $nv->show_note($note_object); =cut sub show_note { my $self = shift; my ($note) = @_; $self->_enable_buttons; $self->be_open; # make a thread my $thread = $note->book->note_thread($note); my $tc = 'dtRdr::NoteThread'; ($thread) = $tc->create($thread) unless($thread->isa($tc)); #WARN "got ", join(",", $thread->rmap(sub {$_->id})); $self->$set_thread($thread); $self->render; } # end subroutine show_note definition ######################################################################## =head2 note_deleted Notify the view that the annotation was deleted. $nv->note_deleted($anno); =cut sub note_deleted { my $self = shift; my ($note) = @_; my $did = $note->id; my @list = grep({(not $_->is_fake) and ($_->id ne $did)} $self->thread->rmap(sub {$_->note})); if(@list) { # there's something left; $self->show_note($list[0]); } else { $self->no_note; } } # end subroutine note_deleted definition ######################################################################## =head2 render Render the current thread. $nv->render; =cut sub render { my $self = shift; my $thread = $self->thread or croak("no thread"); my $title = $thread->note->title; $title = '--' unless defined($title); $self->title_bar->SetLabel($title); # TODO something about this css and html wrapping sillyness my $css = <<'CSS'; h1.title { color: #FF0000; font-size: 15px; } body { color: #000000; font-size: 12px; font-family: Geneva, Arial, Helvetica; background-color: white; margin-top: 0px; margin-left: 0px; } CSS my $page = '' . '' . '' . #'

' . $title . '

' . #'

' . #(defined($content) ? $content : '-- no content --') . #'

' . qq(NOTE: this view is a work in progress.) . join("\n", $self->render_thread($thread)) . ''; DBG_DUMP('NOTEVIEW', 'notes.html', sub {$page}); $self->htmlwidget->SetPage($page); } # end subroutine render definition ######################################################################## my $w = sub { # TODO something something my ($tag, $args, $text) = @_; unless($text) { $text = $args; $args = {}; } my $atts = ''; foreach my $k (keys(%$args)) { $atts .= qq( $k="$args->{$k}"); } return(join("\n", "<$tag$atts>", $text, "" )); }; =head2 render_thread my $html_chunk = $nv->render_thread($thread); =cut sub render_thread { my $self = shift; my ($thread) = @_; my $subref = sub { my ($n) = @_; if($n->is_dummy) { # TODO this fixup should happen elsewhere? my $p = $n->parent; #$p or WARN "no p for me!"; my $title = ($p ? $p->note->title : '---') . ' (missing)'; my $note = $n->note; $note->set_title($title); $note->set_content( '-- note \'' . $note->id . '\' not available --' ); } # prepend indentation my $indent = (' 'x5)x ($n->depth + 1) || ''; #WARN "indent $indent"; return( $w->('tr', $w->('td', $w->('table', $w->('tr', $w->('td', $indent) . $w->('td', $self->render_note($n->note)) ) ) ) . $w->('td', $self->render_meta($n->note)) ) ); }; return($w->('table', {width => '100%'}, join("\n", $thread->rmap($subref)))); } # end subroutine render_thread definition ######################################################################## =head2 render_note my $html_chunk = $nv->render_note($note); =cut sub render_note { my $self = shift; my ($note) = @_; my $title = $note->title; my $content = $note->content; $content = '' unless(defined($content)); my $chunk = $w->('h3', $title) . $content; return($chunk); } # end subroutine render_note definition ######################################################################## =head2 render_meta my $table = $nv->render_meta($note); =cut sub render_meta { my $self = shift; my ($note) = @_; my $id = $note->id; my $p = $note->public; my $user = ($p ? ('' . (defined($p->owner) ? $p->owner : 'you') . ' on ' . $p->server # TODO get servername? ) : 'private' ); my $ts = $note->mod_time || $note->create_time; my ($date, $time); if($ts) { ($date, $time) = $self->date_format($ts); } else { $time = '~sometime~'; $date =''; } my @links; my $href_base = 'dr://NOTEVIEW/'; push(@links, map( {$w->('a', {href => $href_base . "$_/$id"}, $_)} 'reply', ($note->is_fake ? () : (($p and defined($p->owner)) ? () : ('edit', 'delete')) ) )); my $chunk = $w->('table', $w->('tr', $w->('td', join('
', map({$w->('i', $_)} $user, $time, $date)) ) ) . $w->('tr', $w->('td', join('
', @links)) ) ); return($chunk); } # end subroutine render_meta definition ######################################################################## =head1 MoveMe! =head2 date_format my ($date, $time) = $package->date_format($epoch_time); =cut sub date_format { my $self = shift; my ($ts) = @_; # TODO DateTime? # TODO user-configurable formatting (24hr vs am/pm etc) # TODO fancy formatting (Today, Yesterday, etc.) use Date::Format (); my $date = Date::Format::time2str('%m/%d', $ts); my $time = Date::Format::time2str('%I:%M %P', $ts); return($date, $time); } # end subroutine date_format definition ######################################################################## =head1 AUTHOR Eric Wilhelm http://scratchcomputing.com/ =head1 COPYRIGHT Copyright (C) 2006 Eric L. Wilhelm and OSoft, All Rights Reserved. =head1 NO WARRANTY Absolutely, positively NO WARRANTY, neither express or implied, is offered with this software. You use this software at your own risk. In case of loss, no person or entity owes you anything whatsoever. You have been warned. =head1 LICENSE The dotReader(TM) is OSI Certified Open Source Software licensed under the GNU General Public License (GPL) Version 2, June 1991. Non-encrypted and encrypted packages are usable in connection with the dotReader(TM). The ability to create, edit, or otherwise modify content of such encrypted packages is self-contained within the packages, and NOT provided by the dotReader(TM), and is addressed in a separate commercial license. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. =cut # vi:ts=2:sw=2:et:sta 1;