# $Id: Dock.pm,v 0.1.1 2004/07/11 00:00:00 luka Exp $
# This file is part of PerlPanel.
# 
# PerlPanel is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# PerlPanel is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with PerlPanel; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Copyright: (C) 2003-2004 Gavin Brown <gavin.brown@uk.com>
# Copyright: (C) 2004 Luka Frelih <luka@ljudmila.org>
#
# Notes from Luka:
# 	- new file, merging taskbar and iconbar into one ui (comments welcome!)
#	- has better desktop entry standard support
#	- this file is at an early stage (read unstable)
#	- a goal is to avoid the need for user configuration
#	- i wish for discussion on the usability of this now
#
# Todo:
#	- better mouse event handling & menus
#	- visual indication of running state
#	- get rid of bugs
#	- remove app/icon class dependencies of dock
#	- code cleanup
#	- optimization (this last)
#
package PerlPanel::Applet::Dock;
use Gnome2::Wnck;
use Gtk2::SimpleList;
use Data::Dumper;
use strict;

our $VERSION="0.1";

our $DATADIR="$ENV{'HOME'}/.perlpanel/dock";

if (!-d $DATADIR) {
    mkdir $DATADIR;
}

sub new {
    my $self		= {};
    $self->{package}	= shift;
    bless($self, $self->{package});
    return $self;
}

sub app_opened {
#	   print "application opened\n";

    my $self=shift;
    my $screen=shift;
    my $app= shift;

    my @windows=$app->get_windows;

    for (@windows) {

	$self->add_window($_);
    }
    
}

sub app_closed {
#	   print "application closed\n";

    my $self=shift;
    my $screen=shift;
    my $app= shift;

    my @windows=$app->get_windows;

#	   $icon->find_tip;

#	   for (@windows) {
#	       print Dumper($_);
#	       $self->add_window($_);
#	   }
    

}


sub window_opened {
#    print "window opened\n";

    my $self=shift;
    my $screen=shift;
    my $window= shift;

    my $xid=$window->get_xid if defined($window);

    my $group=$window->get_class_group if defined($window);

    my $icon=$self->{icons}->{($group->get_res_class)} if defined($group);

    $self->{window_icons}->{$xid}=$icon;

    $icon->find_tip if defined($icon);

#	   print $self;
#	   print $screen;
#	   print $window;
#	   print $group;
#	   print $icon;
    
}

sub window_closed {
#	   print "window closed\n";
    my $self=shift;
    my $screen=shift;
    my $window= shift;

    my $xid=$window->get_xid if defined($window);
#	   print $xid;

    my $group=$window->get_class_group if defined($window);

    my $icon=$self->{window_icons}->{$xid} if defined($xid);

    $icon->find_tip if defined($icon);

#	   print $self;
#	   print $screen;
#		print $window;
#	   print $group;
#	   print $icon;

}


sub add_window {
    my $self=shift;
    my $window=shift;

#    my $xid=$window->get_xid;
#    printf "add_window xid=0x%x\n",$xid ;

    unless ($window->is_skip_tasklist) {

	my $group= $window->get_class_group;

	unless ($window->get_icon_is_fallback || defined($group->{pixbuf}) ) {
	    $group->{pixbuf}=$window->get_icon;
	    print "storing icon in group\n";
	}
	
	$self->add_group($group);
	
    }
    
}



sub stacking_changed {
#    print "stacking_changed\n";
}


sub window_changed {
    my $self=shift;
    my $screen=shift;

#    print "window_changed\n";
    
    $self->find_topicon;
    
}

sub find_topicon {
    my $self=shift;
    my $screen=$self->{screen};

    my @windows=$screen->get_windows_stacked;
    my $window;

    while ($window=pop @windows) {

	last unless ($window->is_skip_tasklist || $window->is_minimized);
#	print "skipped window ".$window->get_name."\n";
    }

    if (!defined($window)) {
	undef $self->{topicon};
#	print "no windows active\n";
	return;
    }
    
#    print "top window ".$window->get_name."\n";

    if ($window->get_class_group == $self->{topicon}->{group}) {
#	print "still on top\n";
    } else {

	my $newicon=$self->{icons}->{$window->get_class_group->get_res_class};

	$self->{topicon}=$newicon;
#	print "set topicon to ".$newicon->{name}."\n";
	
    }


}

sub configure {

#    print "configure\n";

    my $self = shift;
    $self->{screen} = Gnome2::Wnck::Screen->get_default;
    $self->{screen}->force_update;

    if (defined($self->{widget})) {

    } else {
	$self->{widget} = Gtk2::HBox->new;
	$self->widget->set_spacing(PerlPanel::spacing);
    }

    $self->{screen}->signal_connect('window-opened', sub {
	#(Gnome2::Wnck::Screen, Gnome2::Wnck::Window)	
	my $screen=shift;
	my $win= shift;
	
	$self->window_opened($screen, $win);	
    });

    $self->{screen}->signal_connect('window-closed', sub {
	#(Gnome2::Wnck::Screen, Gnome2::Wnck::Window)	
	my $screen=shift;
	my $win= shift;
	
	$self->window_closed($screen, $win);	
    });
    
    $self->{screen}->signal_connect('application-opened', sub {
	#(Gnome2::Wnck::Screen, Gnome2::Wnck::Application)	
	my $screen=shift;
	my $app= shift;
	
	$self->app_opened($screen, $app);	
    });

    $self->{screen}->signal_connect('application-closed', sub {
	#(Gnome2::Wnck::Screen, Gnome2::Wnck::Application)	
	my $screen=shift;
	my $app= shift;
	
	$self->app_closed($screen, $app);	
    });
    
    $self->{screen}->signal_connect('window-stacking-changed', sub {
	#(Gnome2::Wnck::Screen)	
	my $screen=shift;
	
	$self->stacking_changed($screen);	
    });
    
    $self->{screen}->signal_connect('active-window-changed', sub {
	#(Gnome2::Wnck::Screen)	
	my $screen=shift;
	my $app= shift;
	
	$self->window_changed($screen);	
    });
    


#
# load persistent entries
#
    my @entries=glob("$DATADIR/*.desktop");

    print Dumper(@entries);

    foreach my $entry (@entries) {
	my $de=Desktop::Entry->new($self->{id});
	print "using desktop entry $entry\n";
	$de->parse_file($entry);					

	my $keep=$de->get('X-Dock-Keep');
	
	if ($keep ne 'false') {
	    my $icon=PerlPanel::Applet::Dock::Application->new_from_desktop_entry($de,$self);
	    $self->{icons}->{$icon->get_class}= $icon;

	    $self->widget->pack_start($icon->widget, 0, 0, 0);
	    $self->widget->show_all;
	}
	
    }


# load wnck groups
    my @windows = $self->{screen}->get_windows;
    
    $self->{apps}={};
    $self->{groups}={};
    
#	print Dumper(@windows);

    for (@windows) {

	$self->add_window($_);
	
#	print "window ";
	
#	print $_->get_name ;
#	print ' ('.$_->get_pid.') ' ;
#	print $_->get_xid ;	
#	print $_->get_icon_is_fallback ? " icon_is_fallback" : "";
#	print $_->is_active ? " active" : "";
#	print $_->is_maximized ? " is_maximized" : "";
#	print $_->is_maximized_horizontally ? " is_maximized_horizontally" : "";
#	print $_->is_maximized_vertically ? " is_maximized_vertically" : "";
#	print $_->is_minimized ? " is_minimized" : "";
#	print $_->is_pinned ? " is_pinned" : "";
#	print $_->is_shaded ? " is_shaded" : "";
#	print $_->is_skip_pager ? " is_skip_pager" : "";
#	print $_->is_skip_tasklist ? " is_skip_tasklist" : "";
#	print $_->is_sticky ? " is_sticky" : "";
#	print "\n";

    }
    
    return 1;
}


sub add_group {
    my $self=shift;
    my $group=shift;
    
    print "add_group ".$group->get_name."\n";

    if (defined($self->{icons}->{$group->get_res_class})) {
	my $icon=$self->{icons}->{$group->get_res_class};
	
	if (!defined($icon->{group})) {
	    $icon->{group}=$group;
	    $icon->build_pixmap if $icon->{icon_missing};
	    $icon->find_tip;
	}

    } else {
	$self->add_icon($group);
    }
    
}


# this name sucks
sub add_icon {
    my ($self, $group) = @_;

    my $class=$group->get_res_class;

    if (defined($self->{icons}->{$class})) {
	$self->{icons}->{$class}->{group}=$group;
	return 0;
    }

    my $icon=PerlPanel::Applet::Dock::Application->new_from_group($group,$self);
    $self->{icons}->{$class}=$icon;

    my @windows=$group->get_windows;

    for(@windows) {
	my $xid=$_->get_xid;
	$self->{window_icons}->{$xid}=$icon;
    }

    $self->widget->pack_start($icon->widget, 0, 0, 0);
    $self->widget->show_all;

    return 1;
}

sub widget {
    return $_[0]->{widget};
}

sub expand {
    return 1;
}

sub fill {
    return 1;
}

sub end {
    return 'start';
}

sub get_default_config {
    return undef;
}

package PerlPanel::Applet::Dock::Application;
use Gtk2::Helper;
use Desktop::Entry;
use Data::Dumper;
use strict;

our $DATADIR = ${PerlPanel::Applet::Dock::DATADIR};

#
# an instance of this class represents an application that is
# running or can be run from this dock.  
# each icon in the panel gets its own instance of this class.
#

#
# minimal constructor - pass in a RES_CLASS string
#

sub new {
    my $self		= {};
    $self->{package}	= shift;
    $self->{class} = shift;
    $self->{dock} = shift;

    $self->{id} = lc($self->{class});
    $self->{id} =~ /(\S+)/;
    $self->{id} =~ $1;
    #mozilla apps like this
    $self->{id} =~ s/-bin$//;

    $self->{id}='unknown' unless $self->{id}; 
    chomp($self->{nautilus}	= `which nautilus`);
    bless($self, $self->{package});

    print "new App '$self->{class}'\n";

    return $self;
}

#
# wnck constructor - pass in a wnck classgroup. 
#
# FIXME: should use new to create the object
sub new_from_group {
    my $self		= {};
    $self->{package}	= shift;
    $self->{group}	        = shift;
    $self->{dock} = shift;
    $self->{name} = $self->{group}->get_name;
    $self->{class} = $self->{group}->get_res_class;
    $self->{id} = lc($self->{class});
    $self->{id} =~ /(\S+)/;
    $self->{id} =~ $1;
    #mozilla apps like this
    $self->{id} =~ s/-bin$//;

    $self->{id}='unknown' unless $self->{id}; 

    print "new App from WNCK group '$self->{class}'\n";

    my $cmd;
    my $entries;

    my $storedfilename="$DATADIR/$self->{id}.desktop";

    if (!-f $storedfilename) {
# FIXME: replace with something safer & saner
	$cmd="locate ".$self->{id}.".desktop\n";
#	print "CMD: $cmd\n";
	$entries=`$cmd`;	
    } else {
	$entries = $storedfilename;
    }

    my $entry;

    for $entry (split($/, $entries)) {
#		print "entry: $entry\n";
# parse (needs some validation & iteration?)

	my $de=Desktop::Entry->new($self->{id});

	print "using desktop entry $entry\n";
	$de->parse_file($entry);					
	$self->{de}=$de;

	$self->{name}=$de->get('Name') if $de->get('Name');
	$self->{icon}=$de->get('Icon') if $de->get('Icon');
	$self->{exec}=$de->get_exec if $de->get_exec;
	$self->{keep}=$de->get('X-Dock-Keep') if $de->get('X-Dock-Keep');
	last;
    }

    $self->{iconfile} = PerlPanel::lookup_icon(lc($self->{icon}));

#	print "lookup icon ".$self->{icon}.": '".$self->{iconfile}."'\n";

# only apps have this info
#	$self->{startup_id} = $self->{app}->get_startup_id;
#	$self->{pid} = $self->{app}->get_pid;
#	$self->{exe} = readlink('/proc/'.$self->{pid}.'/exe') if ($self->{pid});
    #debug
#	$self->{comment} = "xid:".$self->{group}->get_windows." pid:".$self->{pid}." exe:".$self->{exe}." startid:".$self->{startup_id};

#	print $self->{name}." ".$self->{comment}.$/;

#	$self->{icondir}	= sprintf('%s/.%s/icons', $ENV{HOME}, lc($PerlPanel::NAME));
    chomp($self->{nautilus}	= `which nautilus`);
    bless($self, $self->{package});

    $self->build;
    return $self;
}



#
# convenience constructor. pass in a Desktop::Entry object.
#
sub new_from_desktop_entry {
    my $package = shift;
    my $de = shift;
    my $dock = shift;
    my $class = $de->get('StartupWMClass');

    if (!$class) {
	print "i want to see the StartupWMClass key\n";
	return 0;
    }

    my $self=PerlPanel::Applet::Dock::Application->new($class, $dock);

    #put this in a method and reuse above!
    $self->{de}=$de;
    $self->{name}=$de->get('Name') if $de->get('Name');
    $self->{icon}=$de->get('Icon') if $de->get('Icon');
    $self->{exec}=$de->get_exec if $de->get_exec;
    $self->{keep}=$de->get('X-Dock-Keep') if $de->get('X-Dock-Keep');

    $self->{iconfile} = PerlPanel::lookup_icon(lc($self->{icon}));
    $self->build;

    return $self;
}



sub widget {
    return $_[0]->{widget};
}



sub build {
    my $self = shift;
    
#	print Dumper($self);

    $self->{widget} = Gtk2::Button->new;
    $self->widget->set_border_width(0);
    $self->widget->set_relief('none');
    
    $self->widget->signal_connect('button_release_event', sub {
	# this mess reconciles the behaviour of 'button_release_event' with the expected behaviour, which
	# should be that of 'clicked'. The clicked() method is only called if the mouse pointer is within
	# the widget (get_pointer() returns the co-ords of the pointer relative to the top left corner of
	# the widget):
	my ($mouse_pos_x, $mouse_pos_y) = $self->widget->get_pointer;
	my $widget_size_x = $self->widget->size_request->width;
	my $widget_size_y = $self->widget->size_request->height;
	if (
	    $mouse_pos_x <= $widget_size_x &&
	    $mouse_pos_y <= $widget_size_y &&
	    $mouse_pos_x > 0 &&
	    $mouse_pos_y > 0
	    
	    ) {
	    $self->clicked($_[1]->button);
	}
	return undef;
    });

    $self->build_pixmap;
    $self->find_tip;

    return 1;
}


sub build_pixmap {
    my $self = shift;

    print "build_pixmap for $self->{name}: ";

    if (-f $self->{iconfile}) {
	print "using file icon\n";
	$self->{pixbuf} = Gtk2::Gdk::Pixbuf->new_from_file($self->{iconfile});

    } elsif (defined($self->{group})) {

	if (defined($self->{group}->{pixbuf})) { 
	    $self->{pixbuf} = $self->{group}->{pixbuf};
	    print "using group pixbuf\n";
	} else {
	    $self->{pixbuf} = $self->{group}->get_icon;
	    print "using group icon\n";
	}

    } 

    print "icon missing\n" unless $self->{pixbuf};

    my $oldpixmap;

    if ($self->{pixmap}) {
	print "ICON CHANGE on $self->{name}\n";
	$oldpixmap=1;
    } else {
	$self->{pixmap} = Gtk2::Image->new_from_stock('gtk-missing-image', PerlPanel::icon_size_name);
	$self->{icon_missing}=1;
    }	

    if ($self->{pixbuf}) {
	#resize if needed 
	my $x0 = $self->{pixbuf}->get_width;
	my $y0 = $self->{pixbuf}->get_height;
	if ($x0 != PerlPanel::icon_size || $y0 != PerlPanel::icon_size) {
	    my ($x1, $y1);
	    if ($x0 > $y0) {
		# image is landscape:
		$x1 = PerlPanel::icon_size;
		$y1 = int(($y0 / $x0) * PerlPanel::icon_size);
	    } elsif ($x0 == $y0) {
		# image is square:
		$x1 = PerlPanel::icon_size;
		$y1 = PerlPanel::icon_size;
	    } else {
		# image is portrait:
		$x1 = int(($x0 / $y0) * PerlPanel::icon_size);
		$y1 = PerlPanel::icon_size;
	    }
	    $self->{pixbuf} = $self->{pixbuf}->scale_simple($x1, $y1, 'bilinear');
	}

	#make the pixmap from pixbuf
	$self->{pixmap}->set_from_pixbuf($self->{pixbuf});
	$self->{icon_missing}=0;

    }

    $self->{pixmap}->set_size_request(PerlPanel::icon_size, PerlPanel::icon_size);
    
    #add (only new) pixmap to button
    $self->widget->add($self->{pixmap}) unless $oldpixmap;
}




sub get_class {
    my $self=shift;
    return $self->{class};
}



sub is_running {
    my $self=shift;

    return 0 if !$self->{group};

    my @windows = $self->{group}->get_windows;
    my $nwindows = scalar(@windows);

    return ($nwindows>=1);
}



#
# find all the right times to call this
#
sub find_tip {

#    print "find tip\n";

    my $self=shift;

    my $tip = $self->{name} || $self->{class};

    if ($self->{group}) {    
    	my @windows = $self->{group}->get_windows;
    	my $nwindows = scalar(@windows);
	
    	$tip .= ' ('.$nwindows.')' if $nwindows>1;
    	$tip .= ' (not running)' if $nwindows==0;

    } else {
	$tip .= ' (not running)';
    }

    $tip .= "\n".$self->{comment} if ($self->{comment} ne '');

  PerlPanel::tips->set_tip($self->widget, $tip);
    
}



sub clicked {
#    print "clicked!\n";

    my ($self, $button) = @_;
    $self->widget->grab_focus;

    if ($button == 1) {
	#first button - bring app to focus

	$self->{dock}->find_topicon;

	#unless its just been focused
#	if ($self->{ontop}) {
	if ($self==$self->{dock}->{topicon}) {
	    $self->hide;
	} else {
	    #bring the app up
	    $self->activate;
# timing issues
	}
    } elsif ($button == 3) {
	#third button - context menu

	$self->make_menu();

	$self->{menu}->popup(undef, undef, sub { return $self->popup_position(@_) }, 0, $self->widget, undef);
	

    }
    
    return 1;
}



sub popup_position {
    my $self = shift;
    my ($x0, undef) = PerlPanel::get_widget_position($self->widget);
    if (PerlPanel::position eq 'top') {
	return ($x0, PerlPanel::panel->allocation->height);
    } else {
	$self->{menu}->realize;
	$self->{menu}->show_all;
	return ($x0, PerlPanel::screen_height() - $self->{menu}->allocation->height - PerlPanel::panel->allocation->height);
    }
}



sub make_menu {

    my $self = shift;

    $self->{itemfactory} = [];

    push @{$self->{itemfactory}},
    (
     [
      '/',
      undef,
      undef,
      undef,
      '<Branch>',
      ],
     [
      '/'._('Activate'),
      undef,
      sub { $self->activate },
    undef,
    '<StockItem>','',
    ],
    [
    '/'._('Quit'),
    undef,
    sub { $self->quit },
    undef,
    '<StockItem>','',
    ],
    [
    '/'._('Hide others'),
    undef,
    sub { $self->hide_others },
    undef,
    '<StockItem>','',
    ],
    [
    '/'._('Hide'),
    undef,
    sub { $self->hide; },
    undef,
    '<StockItem>','',
    ],
    [
    '/'._('Keep'),
    undef,
    sub { $self->keep; },
    4335,
    '<CheckItem>','',
    ],
    [
    '/Separator',
    undef,
    undef,
    undef,
    '<Separator>','',
    ],
    );

if (0 && -x $self->{nautilus}) {
    push(
    @{$self->{itemfactory}},
    [
     '/'._('View Icon Directory'),
     undef,
     sub { system("$self->{nautilus} --no-desktop $self->{icondir} &") },
    undef,
    '<StockItem>',''
    ],
    );
}

if ($self->{group}) {
# enumerate windows
    my @windows=$self->{group}->get_windows;

    for (@windows) {

	my $win=$_;

	my $defanged= $_->get_name;
	$defanged =~ s/\//\\\//g;
#    print "defanged $defanged\n";

	push(
    @{$self->{itemfactory}},
    [
     '/'.$defanged,
     undef,
     sub { $win->unminimize; $win->activate; },
    undef,
    '<StockItem>',''
    ],
    );
}

}

$self->{factory} = Gtk2::ItemFactory->new('Gtk2::Menu', '<main>', undef);
$self->{factory}->create_items(@{$self->{itemfactory}});
$self->{menu} = $self->{factory}->get_widget('<main>');
$self->{keepwidget}= $self->{factory}->get_widget('/Keep');
$self->{keepwidget}->set_active($self->{keep});
}



sub activate {
    my $self = shift;    

    if (!$self->is_running) {

	if ((!$self->{launchtime}) || ($self->{launchtime} < (time-10))) {
	    print "running: ".$self->{exec}."\n";

	    $self->{launchtime}=time;
	    system($self->{exec}.' &');

	} else {
	    print "not running - was launched less than 10 seconds ago\n";
	}

    } else {

	$self->{ontop} = 1;

	#FIXME!
	#this should be done in state-conserving order
	
	if ($self->{group}) {    
	    for($self->{group}->get_windows) {
		$_->unminimize;
		$_->activate;
	    }
	}

	$self->{dock}->{topicon}=$self;
	#    print "set topicon in activate\n";
    }
}



sub hide {
    my $self = shift;

    $self->{ontop}=0;

    if ($self->{group}) {
	for($self->{group}->get_windows) {
# fixme! proper workspace support
#	$_->activate;
	    $_->minimize;
	}
    }
}



sub quit {
    my $self = shift;

    if ($self->{group}) {
	for($self->{group}->get_windows) {
	    $_->close;
	}
    }
}



sub hide_others {
    my $self=shift; 
    my $dock=$self->{dock};
    
    $self->activate;
    
    for (values(%{($dock->{icons})})) {
	$_->hide unless ($_ == $self);
    }
}



sub keep {
#	print "Keep!\n";

    my $self=shift;

    if ($self->{keepwidget}->get_active != $self->{keep}) {
	$self->{keep}=$self->{keepwidget}->get_active;

	if (!$self->{de}) {
	    print "can't keep this yet... create the file \$HOME/.perlpanel/dock/$self->{id}.desktop yourself!\n";
	    return -1;
	}

	$self->{de}->set_group('Desktop Entry');
	$self->{de}->parse_line('StartupWMClass='.$self->{class});

	#this is an desktop entry file extension proposal! 
	#key is X-Dock-Keep, value is boolean
	#the icon will not show up in dock on startup if this key is false
	#missing key will be treated as true
	$self->{de}->parse_line('X-Dock-Keep='.(($self->{keep})?'true':'false'));

	#write updated desktop file	

#	print "X-Dock-Keep=".$self->{de}->get('X-Dock-Keep')."\n";	
	my $filename="$DATADIR/$self->{id}.desktop";
	$self->{de}->write_file($filename);

	return 1;
    }

    return 0;
}

1;
















