#!/usr/bin/perl -w

# $Id: FvwmSaveWindow 315 2014-09-19 15:20:23Z tomas $
#
# Changes:
# 2011-11-15 - Some changes for Fvwm v 2.5.30 (changed getToken to get_token)
# 2014-04-08 - Added saving of desktop number
# 2014-09-19 - Bugfix: crashed when reading file

# FvwmSaveWindow
# Copyright (C) 2003-2014 Tomas Åkesson tomas(at)datasupporten.se
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


use strict;
#$prefix = "@prefix@";
#$datadir = "@datadir@";
#use lib "@FVWM_PERLLIBDIR@";
use lib `fvwm-perllib dir`;
use FVWM::Module;
#use FVWM::Module::Tk;
#use FVWM::Module::Toolkit qw(Tk FileHandle X11::Protocol Tk::Balloon);
use General::Parse;
use IO::File;

my $filename = "window-settings";
my $saveDir = "";
my $configFile = "";
my $curWinId = 0;
my $curAction = "";
my $handlingEvent = 0;
my $curWidth = 0;
my $curHeight = 0;
my $curPosX = 0;
my $curPosY = 0;
my $curDeskNum = 0;
my $debugging = 0;

#my $options = {
#    'file' => \$filename,
#    'd|debug' => \$debugging,
#};

my $module = new FVWM::Module(
    Mask => M_ADD_WINDOW | M_STRING | M_CONFIG_INFO | M_WINDOW_NAME |
                              M_CONFIGURE_WINDOW | M_SENDCONFIG,
    Debug => $debugging,
    Name => "FvwmSaveWindow",
    #EnableOptions => $options,
);


# Process module options
$module->addHandler(M_CONFIG_INFO, sub {
    my ($module, $event) = @_;

    my $modname = $module->name;
    return unless $event->_text =~ /^\*$modname(.*)$/;

    $module->debug("Got " . $event->name . "\n");
    $module->debug("\t$_: " . $event->args->{$_} . "\n")
        foreach sort keys %{$event->args};

    $_ = $1;
    if ( /^file\s+(.*)/i ) {
        # Configuration file
        # Todo: See if the file exists
        $module->debug("Config, file: " . $1 . "\n");
        $configFile = $saveDir."/".$1;
    }

});


# This handler is called when SendToModule is executed.
# Actions:
# save - saves the current windows geometry
# stop - terminates the module [not tested yet]
$module->addHandler(M_STRING, sub {
    my ($module, $event) = @_;
    my $line = $event->_text;
    my ($action, @args) = get_tokens($line);
    $curWinId = $event->_win_id;

    if ($action eq "stop") {
        $module->terminate;
    } elsif ($action eq "save") {
        $curWinId = $event->_win_id;
        $curAction = "save";
        requestWindowList();
    } else {
        $module->debug("Unknown action: " . $action . "\n");
    }
});

# When a new window is added, we save it's geometry and then requests the
# window-list. The execution is continued in the M_WINDOW_NAME-handler.
$module->addHandler(M_ADD_WINDOW, sub {
    my ($module, $event) = @_;
    $curWinId = $event->_win_id;
    $curAction = "add_window";
    $curWidth  = $event->_frame_width;
    $curHeight = $event->_frame_height;
    $curPosX = $event->_frame_x;
    $curPosY = $event->_frame_y;
    $curDeskNum = $event->_desk;

    requestWindowList();
});


# Don't know exactly why this is needed, but we need it so the geometry is
# saved in the global vars when a "save" action is executed (I think..).
$module->addHandler(M_CONFIGURE_WINDOW, sub {
    my ($module, $event) = @_;
    $curWinId = $event->_win_id;
    $curWidth  = $event->_frame_width;
    $curHeight = $event->_frame_height;
    $curPosX = $event->_frame_x;
    $curPosY = $event->_frame_y;
    $curDeskNum = $event->_desk;
});


# Handler for receiving window-names from the WindowList which is requested by
# requestWindowList()
$module->addHandler(M_WINDOW_NAME, sub {
    my ($module, $event) = @_;
    return unless $handlingEvent;

    # The window-id from the WindowList is the same as the selected
    # window (curWinId).
    if ($event->_win_id == $curWinId) {
        my $curWindowName = $event->_name;
        $module->debug("win-name: " .
                       sprintf("0x%07lx %s\n", $event->_win_id, $curWindowName));

        $module->debug("Got " . $event->name . "\n");
        $module->debug("\t$_: " . $event->args->{$_} . "\n")
            foreach sort keys %{$event->args};

        # The action determines what we are going to do with the window.
        if ($curAction eq "add_window") {
            # Get the window-config from the file
            my ($pos_x, $pos_y) = getSavedWindowPos($curWindowName);
            my ($width, $height) = getSavedWindowSize($curWindowName);
            my ($deskNum) = getSavedDeskNum($curWindowName);

            if ($pos_x >= 0 && $pos_y >= 0) {
                $module->send("WindowId $curWinId Move ".$pos_x."p ".$pos_y."p");
            }

            if ($width > 0 && $height > 0) {
                $module->send("WindowId $curWinId Resize ".$width."p ".$height."p");
            }

            if ($deskNum > 0) {
                $module->send("WindowId $curWinId MoveToDesk 0 ".$deskNum);
            }
        } elsif ($curAction eq "save") {
            saveWindow($curWindowName, $curWidth, $curHeight,
                       $curPosX, $curPosY, $curDeskNum);
        }

        # We don't need to check the other window's in the window-list now
        # since we have found the correct one.
        $handlingEvent = 0;
    }
});


$module->addHandler(M_END_WINDOWLIST, sub {
    $handlingEvent = 0;
});


sub requestWindowList() {
    $handlingEvent = 1;
    $module->send("Send_WindowList");
}


# Saves the geometry in the configFile
# Todo: This probably needs to be rewritten a bit, because of ugliness, also
# we maybe want to check the integrity of the file.
sub saveWindow {
    my ($curWindowName, $curWidth, $curHeight, $curPosX, $curPosY,
        $curDeskNum) = @_;
    # Read the old config
    my $fh = new IO::File "< $configFile";
    my $line;
    my $num;
    my @oldConfig = ();

    # Read the old config-file into an array
    if (defined $fh) {
        $module->debug("saving: " . $configFile . "\n");
        while (!$fh->eof()) {
            $line = $fh->getline();
            $/ = "\n";
            chomp($line);
            push @oldConfig, $line;
            $module->debug("line: " . $line . "\n");
        }
        $fh->close;
    }

    # Save the old config, and the new window
    my $savedSize = 0;
    my $savedPosition = 0;
    my $savedDeskNum = 0;
    $module->debug("config: " . $configFile . "\n");
    if ($fh->open("> $configFile")) {
        while (@oldConfig) {
            $line = pop @oldConfig;
            my ($t_winName, $command, @rest) = get_tokens($line);
            $module->debug("pop: " . $t_winName . "\n");

            # If the window exists, update it
            if ($t_winName eq $curWindowName) {
                $module->debug("MATCH: " . $t_winName . "\n");
                $module->debug("command: " . $command . "\n");
                if ($command eq "Resize") {
                    $fh->printf("\"%s\" Resize %dp %dp\n",
                                $curWindowName, $curWidth, $curHeight);
                    $savedSize = 1;
                } elsif ($command eq "Move") {
                    $fh->printf("\"%s\" Move +%dp +%dp\n",
                                $curWindowName, $curPosX, $curPosY);
                    $savedPosition = 1;
                } elsif ($command eq "MoveToDesk") {
                    $fh->printf("\"%s\" MoveToDesk 0 +%d\n",
                                $curWindowName, $curDeskNum);
                    $savedDeskNum = 1;
                }
            } else {
                $fh->printf("%s\n", $line);
            }
        }

        # If they were not updated, add them at the end
        if (!$savedSize) {
            $fh->printf("\"%s\" Resize %dp %dp\n",
                        $curWindowName, $curWidth, $curHeight);
        }
        if (!$savedPosition) {
            $fh->printf("\"%s\" Move +%dp +%dp\n",
                        $curWindowName, $curPosX, $curPosY);
        }
        if (!$savedDeskNum) {
            $fh->printf("\"%s\" MoveToDesk 0 +%d\n",
                        $curWindowName, $curDeskNum);
        }

        close $fh;
    } else {
        $module->debug("Could not open config-file: ".$configFile.
                       " for writing\n");
    }
}


# Get the saved geometry from the configFile
# Todo: This probably needs to be rewritten a bit, because of ugliness, also
# we maybe want to check the integrity of the file.
sub getSavedWindowSize {
    my ($window_name) = @_;
    my $fh = new IO::File "< $configFile";
    my $line;
    my $width = -1;
    my $height = -1;

    if (defined $fh) {
        $module->debug("loading: " . $configFile . "\n");
        while (!$fh->eof()) {
            $line = $fh->getline();
            $/ = "\n";
            chomp($line);
            my ($t_winPat, $command, $w, $h) = get_tokens($line);

            # If the window exists, get the size
            if (defined($command) && $command eq "Resize" && $window_name =~ /$t_winPat/) {
                $module->debug("MATCH resize: " . $t_winPat . "\n");
                $/ = "p";
                chomp($w);
                chomp($h);
                $width = $w;
                $height = $h;
                $module->debug("w/h: " . $w . "/" . $h . "\n");
            }
        }
        $fh->close;
    }

    return ($width, $height);
}

# Get the saved geometry from the configFile
# Todo: This probably needs to be rewritten a bit, because of ugliness, also
# we maybe want to check the integrity of the file.
sub getSavedWindowPos {
    my ($window_name) = @_;
    my $fh = new IO::File "< $configFile";
    my $line;
    my $pos_y = -1;
    my $pos_x = -1;

    if (defined $fh) {
        $module->debug("loading: " . $configFile . "\n");
        while (!$fh->eof()) {
            $line = $fh->getline();
            $/ = "\n";
            chomp($line);
            my ($t_winPat, $command, $x, $y) = get_tokens($line);

            # If the window exists, get the pos
            if (defined($command) && $command eq "Move" && $window_name =~ /$t_winPat/) {
                $module->debug("MATCH move: " . $t_winPat . "\n");
                $/ = "p";
                chomp($x);
                chomp($y);
                $pos_x = $x;
                $pos_y = $y;
            }
        }
        $fh->close;
    }

    return ($pos_x, $pos_y);
}

# Get the saved desk number from the configFile
# Todo: This probably needs to be rewritten a bit, because of ugliness, also
# we maybe want to check the integrity of the file.
sub getSavedDeskNum {
    my ($window_name) = @_;
    my $fh = new IO::File "< $configFile";
    my $line;
    my $deskNum = -1;

    if (defined $fh) {
        $module->debug("loading: " . $configFile . "\n");
        while (!$fh->eof()) {
            $line = $fh->getline();
            $/ = "\n";
            chomp($line);
            my ($t_winPat, $command, $t_deskNum) = get_tokens($line);

            # If the window exists, get the pos
            if (defined($command) && $command eq "MoveToDesk" && $window_name =~ /$t_winPat/) {
                $module->debug("MATCH movetodesk: " . $t_winPat . "\n");
                $deskNum = $t_deskNum;
            }
        }
        $fh->close;
    }

    return $deskNum;
}

###############
# Main program

# Configuration file
$saveDir = $module->userDataDir;
$configFile = $saveDir."/".$filename;
$module->send("Send_ConfigInfo");

# finally enter event loop
$module->eventLoop;

__END__

# ----------------------------------------------------------------------------
# man page

=head1 DESCRIPTION

This module let's the user save the size and position of windows which doesn't
save it themselves.

The module is started at the beginning of the session and lies in the
background waiting for new windows to open. When a new window opens,
FvwmSaveWindow looks in it's configuration-file to see if the window has saved
geometry, and in that case resizes and moves the window accordingly.

To save the geometry of a window, you must call the module from for example a
menu or a key (see below for examples).

=head1 USAGE

Add a line to start the module in your I<.fvwm2rc>, maybe in the
StartFunction.

    AddToFunc StartFunction
     + "I" Module FvwmSaveWindow

To save the state of a window add something like this to your I<.fvwm2rc>:

    Key F1 A C SendToModule FvwmSaveWindow save

or:

    AddToMenu Window-Ops
     + "Save window geometry" SendToModule FvwmSaveWindow save

The one recognized action is B<save>, which (surprise, surprise) saves the
geometry of the window.

Set the name of the save-file:

    *FvwmSaveWindow: File window-settings

The file is saved in your fvwm-data-directory, i.e. I<~/.fvwm/>, so ensure
you dont have a file called I<window-settings> there already.

=head1 BUGS/TODO

Clean up the code.
Better man-page.
Fix the automake stuff so it can be compiled inside the fvwm-source-tree.
Add a function to remove windows from the config-file.

=head1 SEE ALSO

This module is inspired by FvwmEvent.

=head1 AUTHOR

Tomas Åkesson <tomas@datasupporten.se>.

=cut
