#!/usr/bin/perl # # loCal.pl-v0.27: # loCal (logan's Calendar) is a simple Gtk-Perl calendar (and todo-list) # featuring single or multi-day events as well as event alarms. # # If you are looking for a more fully-featured calendar, check out # Evolution, GNOME-PIM or KDEPIM (Korganizer). # # Requirements: # Perl - http://www.perl.org/ --- tested with 5.6.1 # Gtk-Perl - http://www.gtkperl.org/ --- tested with 0.7008 # # Files Created: # ~/.loCal/calendar : calendar data # ~/.loCal/todolist : todo-list data # # Todo?: # Much optimization. # Recurring entries would be nice. # Support for (ical type) holiday files would be sweet. # A general configuration window may not be a bad idea. # # ChangeLog: # See the bottom of the script. # # Copyright (c) 2002 - 2003 Mike Hokenson # This application is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. use POSIX qw(strftime); use Date::Manip; use Gtk; use strict; # location of calendar and todo-list data files my $loCal_home = $ENV{'HOME'} . "/.loCal"; my $loCal_rc = $loCal_home . "/rc"; my $loCal_calendar = $loCal_home . "/calendar"; my $loCal_todolist = $loCal_home . "/todolist"; # delete calendar entries once they have passed (default; set to 0 to keep) my $delete_old = 1; # use 12 hour clock for handling entries (set to 0 for 24 hour) my $twelve_hour = 1; # timeframe of your day. this is ONLY used for all day events. customize to # fit your needs. my $day_start = 8; # 8:00 AM my $day_end = 17; # 5:00 PM # if $use_alarm is set to 1, a small notification window will appear prior to # the event's passing. Note: this only works WHILE loCal is running. my $use_alarm = 1; # strftime(3) formats for date and time (used for various labels) my $date_format = "%A, %b %e, %Y"; # Monday, Jan 6, 2003 my $time_format = "%l:%M %p"; # 11:16 PM # set/save the location of the calendar. I think this is annoying, but someone # may like it. change to 1 to enable. my $remember_location = 0; ### ### Bring on the nasty! ### Date_Init(); init Gtk; set_locale Gtk; # signals $SIG{'TERM'} = $SIG{'QUIT'} = $SIG{'INT'} = sub { loCal_quit(); }; my @priorities = ("unknown", "very high", "high", "medium", "low", "very low"); my @status = ("unknown", "paused", "waiting", "research", "ready"); my $false = 0; my $true = 1; my $window; my $main_vbox; my $view_container; my $view_box; my $calendar; my $button; my $toggle_button; my $status_button; # configuration (window size, location) my %config; # misc internal data for current calendar and todo-list selection my %data; # contains the actual calendar and todo-list entries my %calendar_entries; my %todolist_entries; my $default_w = 650; my $default_h = 240; $config{'window_x'} = undef; $config{'window_y'} = undef; $config{'window_w'} = $default_w; $config{'window_h'} = $default_h; config_load(); # read the the calendar and todo-list data files calendar_load(); todolist_load(); # the calendar is the default view. note: modifying this will not make the # todo-list start up first. $data{'view'} = "calendar"; $view_container = new Gtk::Container("Gtk::VBox"); $window = new Gtk::Window("toplevel"); $window->set_title("loCal"); $window->set_usize($default_w, $default_h); $window->border_width(5); # allow resize? #$window->set_policy($false, $false, $false); $window->set_default_size($config{'window_w'}, $config{'window_h'}); if($remember_location == $true && defined($config{'window_x'}) && defined($config{'window_y'})) { $window->set_uposition($config{'window_x'}, $config{'window_y'}); } $window->signal_connect("destroy", sub { loCal_quit(); }); $window->signal_connect("configure_event", \&window_configure_event); $main_vbox = new Gtk::VBox($false, 0); $window->add($main_vbox); $main_vbox->add(make_calendar_panel()); $button = make_button("Close"); $button->signal_connect("clicked", sub { loCal_quit(); }); $main_vbox->pack_start($button, $false, $false, 0); $button->can_default($true); $button->grab_default(); $window->show_all(); # check for alarms and/or old entries every 5 minutes Gtk->timeout_add(300000, \&calendar_entries_check); # run one check immediately calendar_entries_check(); main Gtk; exit(0); ### ### Supporting functions ### sub make_calendar_panel { my $hbox; my $vbox; $hbox = new Gtk::HBox($false, 5); $vbox = new Gtk::VBox($false, 0); $hbox->pack_start($vbox, $false, $false, 0); $toggle_button = make_button("Toggle Todo-List"); $toggle_button->set_relief("none"); $toggle_button->signal_connect("clicked", sub { toggle_cal_todo(1); }); $vbox->pack_start($toggle_button, $false, $false, 5); $calendar = new Gtk::Calendar(); $calendar->signal_connect("day_selected", \&calendar_view); $calendar->signal_connect("month_changed", \&calendar_month_changed); # selects the current month and day calendar_view_refresh(); $vbox->add($calendar); $status_button = make_button(""); $status_button->set_relief("none"); $status_button->signal_connect("clicked", \&calendar_view_refresh); calendar_statusbutton_refresh(); Gtk->timeout_add(1000, \&calendar_statusbutton_refresh); $vbox->pack_start($status_button, $false, $false, 5); $vbox = new Gtk::VBox($false, 0); $hbox->add($vbox); $vbox->add($view_container); $vbox->pack_start(make_main_buttons(), $false, $false, 5); return($hbox); } sub toggle_cal_todo { my $in = $_[0]; my $button_face; if($data{'view'} eq "todolist") { $button_face = "Toggle Todo-List"; calendar_view() if($in == 1); } else { $button_face = "Toggle Calendar"; todolist_view() if($in == 1); } $toggle_button->child->set($button_face); } sub todolist_sort { my $a_todo = get_todolist_entry($a); my $b_todo = get_todolist_entry($b); if($a_todo->{'pri'} == $b_todo->{'pri'}) { if($a_todo->{'status'} == $b_todo->{'status'}) { if($a_todo->{'time'} == $b_todo->{'time'}) { return $a_todo->{'slot'} <=> $b_todo->{'slot'}; } else { return $a_todo->{'time'} <=> $b_todo->{'time'}; } } else { return $b_todo->{'status'} <=> $a_todo->{'status'}; } } else { return $a_todo->{'pri'} <=> $b_todo->{'pri'}; } } # displays todo-list entries sub todolist_view { my $label; my $scrolled_window; my $clist; my $key; my $count = -1; $data{'view'} = "todolist"; $view_container->remove($view_box) if(defined($view_box)); $view_box = new Gtk::VBox($false, 0); $view_container->add($view_box); $label = new Gtk::Label("Todo-List"); $label->set_alignment(0.5, 0.5); $view_box->pack_start($label, $false, $false, 0); $scrolled_window = new Gtk::ScrolledWindow("", ""); $scrolled_window->border_width(0); $scrolled_window->set_policy("automatic", "always"); $view_box->add($scrolled_window); $clist = new_with_titles Gtk::CList(("Priority", "Status", "Description", "Added")); $scrolled_window->add($clist); $clist->column_titles_passive(); $clist->set_shadow_type("none"); $clist->set_selection_mode("single"); $clist->set_column_width(0, 65); $clist->set_column_width(1, 65); $clist->set_column_width(2, 210); $clist->set_column_width(3, 100); $clist->set_column_justification(0, "right"); $clist->set_column_justification(1, "right"); $clist->set_column_justification(2, "left"); $clist->set_column_justification(3, "left"); $clist->signal_connect("select_row", \&todo_select_row_callback); $clist->freeze(); foreach $key (sort todolist_sort (keys(%todolist_entries))) { my $e = get_todolist_entry($key); next unless($e); next unless(defined($e->{'data'})); my $p = $priorities[$e->{'pri'}]; my $s = $status[$e->{'status'}]; $count = 0 if($count == -1); $clist->append($p, $s, $e->{'data'}, strftime($date_format, localtime($e->{'time'}))); $data{'t_c'}{'k'}{$count++} = $key; } $clist->thaw(); $view_container->show_all(); $data{'t_c'}{'s'} = undef; $data{'t_c'}{'c'} = $count; sub todo_select_row_callback { my ($clist, $row, $column, $event) = @_; $data{'t_c'}{'s'} = $data{'t_c'}{'k'}{$row}; todolist_edit(1) if($event->{'type'} eq "2button_press"); } } sub make_main_buttons { my $hbox; my $button; my $tooltip; $tooltip = new Gtk::Tooltips(); $hbox = new Gtk::HBox($true, 5); $button = make_button("Add"); $button->signal_connect("clicked", \&button_add); $hbox->add($button); $button = make_button("Edit"); $button->signal_connect("clicked", \&button_edit); $hbox->add($button); $button = make_button("Delete"); $button->signal_connect("clicked", \&button_delete); $hbox->add($button); $button = make_button("Clear"); $button->signal_connect("clicked", \&button_clear); $tooltip->set_tip($button, "Clear Calendar (selected day only) or Todo-List.", ""); $hbox->add($button); return($hbox); } sub button_add { if($data{'view'} eq "todolist") { todolist_edit(0); } else { calendar_edit(0); } } sub button_edit { if($data{'view'} eq "todolist") { todolist_edit(1); } else { calendar_edit(1); } } sub button_delete { if($data{'view'} eq "todolist") { todolist_del(); } else { calendar_del(); calendar_month_changed(); } } sub button_clear { if($data{'view'} eq "todolist") { todolist_clear_prompt(); } else { calendar_clear_prompt(); } } sub calendar_statusbutton_refresh { my $button_face = strftime(" $date_format - $time_format ", localtime()); $status_button->child->set($button_face); return(1); } sub calendar_view_refresh { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); $year += 1900; $calendar->select_month($mon, $year); $calendar->select_day($mday); } sub get_selected_date { my ($year, $month, $day) = $calendar->get_date(); return(($year, $month + 1, $day)); } # clears all, then marks any days on the calendar that contain an entry sub calendar_month_changed { my ($year, $month, $day) = get_selected_date(); my $key; my @localtime = localtime(); my $max_days = Time::DaysInMonth::days_in($year, $month); my $u_s_time = UnixDate(ParseDate("$month/1/$year"), "%s") - 86400; my $u_e_time = $u_s_time + 86400; my %mark_days; my $i; M: for($i = 1; $i <= $max_days; $i++) { $u_s_time += 86400; $u_e_time += 86400; # day is already marked next M if(defined($mark_days{$i})); F: foreach $key (sort(keys(%calendar_entries))) { my $e = get_calendar_entry($key); my $r = calendar_view_is_valid_day($e, $year, $month, $i, $u_s_time, $u_e_time, @localtime); next F if($r == $false); $mark_days{$i} = $true; next M; } } # hm, i still see some flickering $calendar->freeze(); # clear everything first $calendar->clear_marks(); foreach $key (keys(%mark_days)) { $calendar->mark_day($key); } $calendar->thaw(); } sub get_calendar_entry { $_ = $_[0]; my $entry; # no key, adding a new entry unless(defined($_)) { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(); $entry->{'sy'} = $year + 1900; $entry->{'sM'} = $mon + 1; $entry->{'sd'} = $mday; $entry->{'sh'} = $hour; $entry->{'sm'} = 0; $entry->{'ey'} = $year + 1900; $entry->{'eM'} = $mon + 1; $entry->{'ed'} = $mday; $entry->{'eh'} = ($hour == 23) ? 23 : $hour + 1; $entry->{'em'} = ($hour == 23) ? 45 : 0; if($twelve_hour == 1) { $entry->{'sp'} = ($entry->{'sh'} >= 12) ? "PM" : "AM"; $entry->{'ep'} = ($entry->{'eh'} >= 12) ? "PM" : "AM"; } $entry->{'type'} = "normal"; $entry->{'data'} = undef; $entry->{'alrm'} = $false; $entry->{'alrm_tm'} = 15; $entry->{'all_day'} = $false; return($entry); } return($entry) unless(defined($calendar_entries{$_})); $entry->{'sy'} = $calendar_entries{$_}{'sy'}; $entry->{'sM'} = $calendar_entries{$_}{'sM'}; $entry->{'sd'} = $calendar_entries{$_}{'sd'}; $entry->{'sh'} = $calendar_entries{$_}{'sh'}; $entry->{'sm'} = $calendar_entries{$_}{'sm'}; $entry->{'ey'} = $calendar_entries{$_}{'ey'}; $entry->{'eM'} = $calendar_entries{$_}{'eM'}; $entry->{'ed'} = $calendar_entries{$_}{'ed'}; $entry->{'eh'} = $calendar_entries{$_}{'eh'}; $entry->{'em'} = $calendar_entries{$_}{'em'}; if($twelve_hour == 1) { my $sh = $calendar_entries{$_}{'sh'}; my $eh = $calendar_entries{$_}{'eh'}; $entry->{'sp'} = ($sh >= 12) ? "PM" : "AM"; $entry->{'ep'} = ($eh >= 12) ? "PM" : "AM"; } $entry->{'data'} = $calendar_entries{$_}{'data'}; $entry->{'type'} = $calendar_entries{$_}{'type'}; $entry->{'alrm'} = $calendar_entries{$_}{'alrm'}; $entry->{'alrm_tm'} = $calendar_entries{$_}{'alrm_tm'}; $entry->{'all_day'} = $calendar_entries{$_}{'all_day'}; $entry->{'us'} = $calendar_entries{$_}{'us'}; $entry->{'ue'} = $calendar_entries{$_}{'ue'}; return($entry); } sub get_todolist_entry { $_ = $_[0]; my $entry; return($entry) unless(defined($_)); my ($pri, $time, $slot); if(m/^(\d):(\d+):(\d)$/) { $pri = $1; $time = $2; $slot = $3; } return($entry) unless(defined($pri)); $entry->{'pri'} = $pri; $entry->{'time'} = $time; $entry->{'slot'} = $slot; $entry->{'data'} = $todolist_entries{$_}{'data'}; $entry->{'status'} = $todolist_entries{$_}{'status'}; return($entry); } # displays calendar entries for the selected day sub calendar_view { my ($year, $month, $day) = get_selected_date(); my $label; my $scrolled_window; my $clist; my $key; my $count = -1; my @localtime = localtime(); my $u_s_time = UnixDate(ParseDate("$month/$day/$year"), "%s"); my $u_e_time = ($u_s_time + 86400) - 1; toggle_cal_todo(0) if($data{'view'} eq "todolist"); $data{'view'} = "calendar"; $data{'date'} = calendar_view_get_date($year, $month, $day); $view_container->remove($view_box) if(defined($view_box)); $view_box = new Gtk::VBox($false, 0); $view_container->add($view_box); $label = new Gtk::Label($data{'date'}); $label->set_alignment(0.5, 0.5); $view_box->pack_start($label, $false, $false, 0); $scrolled_window = new Gtk::ScrolledWindow("", ""); $scrolled_window->border_width(0); $scrolled_window->set_policy("automatic", "always"); $view_box->add($scrolled_window); $clist = new_with_titles Gtk::CList(("", "", "Time-Frame", "Description")); $scrolled_window->add($clist); $clist->column_titles_passive(); $clist->set_shadow_type("none"); $clist->set_selection_mode("single"); $clist->set_column_width(0, 5); $clist->set_column_width(1, 5); $clist->set_column_width(2, 130); $clist->set_column_width(3, 215); $clist->set_column_justification(0, "center"); $clist->set_column_justification(1, "center"); $clist->set_column_justification(2, "right"); $clist->set_column_justification(3, "left"); $clist->signal_connect("select_row", \&calendar_select_row_callback); $clist->freeze(); foreach $key (sort(keys(%calendar_entries))) { my $e = get_calendar_entry($key); my $sy = $e->{'sy'}; my $sM = $e->{'sM'}; my $sd = $e->{'sd'}; my $sh = $e->{'sh'}; my $sm = $e->{'sm'}; my $sp = $e->{'sp'}; my $ey = $e->{'ey'}; my $eM = $e->{'eM'}; my $ed = $e->{'ed'}; my $eh = $e->{'eh'}; my $em = $e->{'em'}; my $ep = $e->{'ep'}; my $r = calendar_view_is_valid_day($e, $year, $month, $day, $u_s_time, $u_e_time, @localtime); next if($r == $false); my $tick_start = ""; my $tick_end = ""; if($r == -1) { $tick_end = ">"; } elsif($r == -2) { $tick_start = "<"; } elsif($r == -3) { $tick_start = "<"; $tick_end = ">"; } $count = 0 if($count == -1); # gozer was here if($twelve_hour == 1) { $sh -= 12 if($sh >= 12); $eh -= 12 if($eh >= 12); $sh = 12 if($sh == 0); $eh = 12 if($eh == 0); # blah $sh = sprintf("%1d", $sh); $eh = sprintf("%1d", $eh); $clist->append($tick_start, $tick_end, "$sh:$sm $sp - $eh:$em $ep", $e->{'data'}); } else { $clist->append($tick_start, $tick_end, "$sh:$sm - $eh:$em", $e->{'data'}); } $data{'c_c'}{'k'}{$count++} = $key; } $clist->thaw(); $view_container->show_all(); $data{'c_c'}{'s'} = undef; $data{'c_c'}{'c'} = $count; sub calendar_select_row_callback { my ($clist, $row, $column, $event) = @_; $data{'c_c'}{'s'} = $data{'c_c'}{'k'}{$row}; calendar_edit(1) if($event->{'type'} eq "2button_press"); } } sub calendar_view_is_valid_day { my ($e, $year, $month, $day, $u_s_time, $u_e_time, @localtime) = @_; my ($sec,$min,$hour,$mday,$mon,$yr,$wday,$yday,$isdst) = @localtime; $yr += 1900; $mon += 1; # entry is completely within the currently selected day return($true) if($e->{'us'} >= $u_s_time && $e->{'ue'} <= $u_e_time); # entry starts on the selected day, but ends in the future return(-1) if($e->{'sy'} == $year && $e->{'sM'} == $month && $e->{'sd'} == $day && ($e->{'ey'} >= $year || $e->{'eM'} >= $month || $e->{'ed'} >= $day)); # below here it's easiest to use unix times # entry begins before the selected day, but ends on the selected day return(-2) if($e->{'us'} < $u_s_time && ($e->{'ue'} <= $u_e_time && $e->{'ue'} >= $u_s_time)); # entry begins prior to the selected day and ends after return(-3) if($e->{'us'} < $u_s_time && $e->{'ue'} > $u_e_time); return($false); } sub calendar_view_get_date { my ($year, $month, $day) = @_; return(strftime($date_format, localtime(UnixDate(ParseDate("$month/$day/$year"), "%s")))); } # prompts for the remove of all calendar entries for the selected day sub calendar_clear_prompt { my ($year, $month, $day) = get_selected_date(); my $window; my $vbox; my $hbox; my $label; my $button; return if($data{'c_c'}{'c'} < 0); $window = new Gtk::Window("toplevel"); $window->set_title("loCal: Clear " . $data{'date'} . "?"); $window->border_width(5); $window->set_usize(275, 0); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $window->add($vbox); $label = new Gtk::Label("Clear all entries for " . $data{'date'} ."?"); $label->set_alignment(0.5, 0.5); $vbox->add($label); $hbox = new Gtk::HBox($true, 5); $vbox->add($hbox); $button = make_button("Yes"); $button->signal_connect("clicked", sub { calendar_clear(); calendar_month_changed(); $window->destroy(); }); $hbox->add($button); $button = make_button("No"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $window->show_all(); } # removes all calendar entries for the selected day, but only entries that # start on the selected day. multi-day entries should be manually deleted. sub calendar_clear { my ($year, $month, $day) = get_selected_date(); my $key; foreach $key (keys(%calendar_entries)) { my $e = get_calendar_entry($key); next unless($e->{'sy'} == $year && $e->{'sM'} == $month && $e->{'sd'} == $day); calendar_delete_entry($key); } calendar_view(); calendar_save(); } # prompts for the remove of all calendar entries for the selected day sub todolist_clear_prompt { my $window; my $vbox; my $hbox; my $label; my $button; return if($data{'t_c'}{'c'} < 0); $window = new Gtk::Window("toplevel"); $window->set_title("loCal: Clear Todo-List?"); $window->border_width(5); $window->set_usize(275, 0); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $window->add($vbox); $label = new Gtk::Label("Clear Todo-List?"); $label->set_alignment(0.5, 0.5); $vbox->add($label); $hbox = new Gtk::HBox($true, 5); $vbox->add($hbox); $button = make_button("Yes"); $button->signal_connect("clicked", sub { todolist_clear(); $window->destroy(); }); $hbox->add($button); $button = make_button("No"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $window->show_all(); } # removes all calendar entries for the selected day sub todolist_clear { %todolist_entries = (); todolist_view(); todolist_save(); } sub calendar_delete_entry { my $key = $_[0]; return unless(defined($key)); delete $calendar_entries{$key}; } # removes the calendar entry sub calendar_del { calendar_delete_entry($data{'c_c'}{'s'}); calendar_view(); calendar_save(); } # removes the todo-list entry sub todolist_del { my $key = $data{'t_c'}{'s'}; return unless(defined($key)); delete $todolist_entries{$key}; todolist_view(); todolist_save(); } # edits the selected calendar entry sub calendar_edit { my $in = $_[0]; my $key; $key = undef; $key = $data{'c_c'}{'s'} if($in == 1); return if($in == 1 && !defined($key)); # window title my $type = ($in == 1) ? "Edit" : "Add"; my ($year, $month, $day) = get_selected_date(); my $e = get_calendar_entry($key); my $sy = $e->{'sy'}; my $sM = $e->{'sM'}; my $sd = $e->{'sd'}; my $sh = $e->{'sh'}; my $sm = $e->{'sm'}; my $sp = $e->{'sp'}; my $ey = $e->{'ey'}; my $eM = $e->{'eM'}; my $ed = $e->{'ed'}; my $eh = $e->{'eh'}; my $em = $e->{'em'}; my $ep = $e->{'ep'}; $data{'s'}{'alrm'} = $e->{'alrm'}; $data{'s'}{'alrm_tm'} = $e->{'alrm_tm'}; $data{'s'}{'all_day'} = $e->{'all_day'}; unless(defined($key)) { $sy = $year; $sM = $month; $sd = $day; $ey = $year; $eM = $month; $ed = $day; } $data{'s'}{'sy'} = $sy; $data{'s'}{'sM'} = $sM; $data{'s'}{'sd'} = $sd; $data{'s'}{'ey'} = $ey; $data{'s'}{'eM'} = $eM; $data{'s'}{'ed'} = $ed; my $window; my $vbox; my $hbox; my $hbox2; my $frame; my $label; my $entry; my $button; my $tooltip; my $calendar_button_start; my $alarm_spinner; my %items; my $button_face_start_date; my $button_face_end_date; $tooltip = new Gtk::Tooltips(); $window = new Gtk::Window("toplevel"); $window->set_title("loCal: $type Entry"); $window->border_width(5); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $window->add($vbox); $label = new Gtk::Label("Start:"); $label->set_alignment(0, 0); $vbox->add($label); $hbox = new Gtk::HBox($false, 5); $button_face_start_date = calendar_view_get_date($sy, $sM, $sd); $items{'bs'}= make_button($button_face_start_date); $items{'bs'}->set_usize(175, 0); $items{'bs'}->signal_connect("clicked", \&calendar_edit_start_day_sel); $hbox->pack_start($items{'bs'}, $false, $false, 5); $items{'se'} = make_time_entry("s", $sh, $sm); $hbox->pack_start($items{'se'}, $false, $false, 0); if($twelve_hour == 1) { $items{'sp'} = make_ampm_option("sp", $sp); $hbox->pack_start($items{'sp'}, $false, $false, 0); } $vbox->add($hbox); $label = new Gtk::Label("End:"); $label->set_alignment(0, 0); $vbox->add($label); $hbox = new Gtk::HBox($false, 5); $button_face_end_date = calendar_view_get_date($ey, $eM, $ed); $items{'be'} = make_button($button_face_end_date); $items{'be'}->set_usize(175, 0); $items{'be'}->signal_connect("clicked", \&calendar_edit_end_day_sel); $hbox->pack_start($items{'be'}, $false, $false, 5); $items{'ee'} = make_time_entry("e", $eh, $em); $hbox->pack_start($items{'ee'}, $false, $false, 0); if($twelve_hour == 1) { $items{'ep'} = make_ampm_option("ep", $ep); $hbox->pack_start($items{'ep'}, $false, $false, 0); } $button = new Gtk::CheckButton("All Day Event"); $button->signal_connect("clicked", \&calendar_edit_toggle_all_day, %items); $button->set_active($e->{'all_day'}); my $ds = $day_start; my $de = $day_end; my $msg = "Make this an All Day Event, running from "; if($twelve_hour == 1) { my $dsp = "AM"; my $dep = "AM"; if($ds >= 12) { $ds -= 12; $dsp = "PM"; } if($de >= 12) { $de -= 12; $dep = "PM"; } $ds = 12 if($ds == 0); $de = 12 if($de == 0); $msg .= "$ds:00 $dsp to $de:00 $dep."; } else { $msg .= "$ds:00 to $de:00."; } $tooltip->set_tip($button, $msg, ""); $hbox->add($button); $vbox->add($hbox); $hbox = new Gtk::HBox($false, 5); $frame = new Gtk::Frame("Event Description:"); $frame->set_shadow_type("none"); $hbox->add($frame); $hbox2 = new Gtk::HBox($false, 5); $frame->add($hbox2); # kinda ugly to have this big nasty signal_connect, then the same # for the Save button, but it's nice to be able to just hit enter to # save the entry, especially quick when editing. $entry = new Gtk::Entry(255); $entry->signal_connect("activate", sub { calendar_edit_close($year, $month, $day, $key, $entry, $window); }); # select the current entry (again, nice when editing) if($key) { $entry->set_text($e->{'data'}); $entry->select_region(0, length($e->{'data'})); } $hbox2->add($entry); if($use_alarm == 1) { $frame = new Gtk::Frame("Alarm:"); $frame->set_shadow_type("none"); $hbox->pack_start($frame, $false, $false, 0); $hbox2 = new Gtk::HBox($false, 5); $frame->add($hbox2); $alarm_spinner = make_alarm_spinner("alrm_tm", $e->{'alrm_tm'}); $alarm_spinner->set_sensitive($e->{'alrm'}); $tooltip->set_tip($alarm_spinner, "Prior notification time (minutes).", ""); $hbox2->add($alarm_spinner); $button = new Gtk::CheckButton("Enable Alarm"); $button->signal_connect("clicked", \&calendar_edit_toggle_alarm, $alarm_spinner); $button->set_active($e->{'alrm'}); $tooltip->set_tip($button, "Display a notification prior to the event's passing.", ""); $hbox2->add($button); } $vbox->add($hbox); $hbox = new Gtk::HBox($true, 5); $button = make_button("Save"); $button->signal_connect("clicked", sub { calendar_edit_close($year, $month, $day, $key, $entry, $window); }); $hbox->add($button); $button = make_button("Cancel"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $vbox->add($hbox); $entry->grab_focus(); $window->show_all(); } # update the calendar if there's valid entries, otherwise display a warning # message (handled in calendar_update()). sub calendar_edit_close { my ($year, $month, $day, $key, $entry, $window) = @_; return if(calendar_update($year, $month, $day, $key, $entry) == 0); calendar_month_changed(); $window->destroy(); } # XXX: merge calendar_edit_start_day_sel and calendar_edit_end_day_sel ? sub calendar_edit_start_day_sel { my $b = $_[0]; my $window; my $vbox; my $hbox; my $calendar; my $button; $window = new Gtk::Window("toplevel"); $window->set_title("loCal: Start Date Selection"); $window->border_width(5); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $calendar = new Gtk::Calendar(); $calendar->select_month(($data{'s'}{'sM'} - 1), $data{'s'}{'sy'}); $calendar->select_day($data{'s'}{'sd'}); $calendar->signal_connect("day_selected_double_click", \&_calendar_edit_start_day_sel, $window, $b); $vbox->add($calendar); $hbox = new Gtk::HBox($true, 5); $button = make_button("Set"); $button->signal_connect("clicked", sub { _calendar_edit_start_day_sel($calendar, $window, $b); }); $hbox->add($button); $button = make_button("Cancel"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $vbox->add($hbox); $window->add($vbox); $window->show_all(); sub _calendar_edit_start_day_sel { my ($calendar, $window, $button) = @_; my ($year, $month, $day) = $calendar->get_date(); $month += 1; my $button_face = calendar_view_get_date($year, $month, $day); $data{'s'}{'sy'} = $year; $data{'s'}{'sM'} = $month; $data{'s'}{'sd'} = $day; $button->child->set($button_face); $window->destroy(); } } sub calendar_edit_end_day_sel { my $b = $_[0]; my $window; my $vbox; my $hbox; my $calendar; my $button; $window = new Gtk::Window("toplevel"); $window->set_title("loCal: End Date Selection"); $window->border_width(5); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $calendar = new Gtk::Calendar(); $calendar->select_month(($data{'s'}{'eM'} - 1), $data{'s'}{'ey'}); $calendar->select_day($data{'s'}{'ed'}); $calendar->signal_connect("day_selected_double_click", \&_calendar_edit_end_day_sel, $window, $b); $vbox->add($calendar); $hbox = new Gtk::HBox($true, 5); $button = make_button("Set"); $button->signal_connect("clicked", sub { _calendar_edit_end_day_sel($calendar, $window, $b); }); $hbox->add($button); $button = make_button("Cancel"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $vbox->add($hbox); $window->add($vbox); $window->show_all(); sub _calendar_edit_end_day_sel { my ($calendar, $window, $button) = @_; my ($year, $month, $day) = $calendar->get_date(); $month += 1; my $button_face = calendar_view_get_date($year, $month, $day); $data{'s'}{'ey'} = $year; $data{'s'}{'eM'} = $month; $data{'s'}{'ed'} = $day; $button->child->set($button_face); $window->destroy(); } } sub calendar_edit_toggle_all_day { my ($button, %items) = @_; $data{'s'}{'all_day'} = $button->active; my $s = opp($data{'s'}{'all_day'}); my $key; foreach $key (keys(%items)) { $items{$key}->set_sensitive($s); } } sub calendar_edit_toggle_alarm { my ($button, $spinner) = @_; $data{'s'}{'alrm'} = $button->active; $spinner->set_sensitive($data{'s'}{'alrm'}); } sub make_alarm_spinner { my ($i, $p) = @_; my $label; my $adj; my $spinner; $data{'s'}{$i} = $p; $adj = new Gtk::Adjustment($p, 5, 240, 5, 30, 0); $spinner = new Gtk::SpinButton($adj, 0, 0); $spinner->set_wrap($true); $spinner->set_shadow_type("in"); $adj->signal_connect("value_changed", \&alarm_spinner_changed, $spinner, $i); return($spinner); sub alarm_spinner_changed { my ($widget, $spinner, $i) = @_; $data{'s'}{$i} = $spinner->get_value_as_int(); } } # edits the selected todo-list entry sub todolist_edit { my $in = $_[0]; my $key; $key = undef; $key = $data{'t_c'}{'s'} if($in == 1); return if($in == 1 && !defined($key)); # window title my $type = ($in == 1) ? "Edit" : "Add"; my $e = get_todolist_entry($key); # set default priority for Add (medium) $e->{'pri'} = 3 unless($e); my $window; my $vbox; my $hbox; my $frame; my $label; my $entry; my $button; $window = new Gtk::Window("toplevel"); $window->set_title("loCal: $type Todo-List Entry"); $window->border_width(5); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $window->add($vbox); $hbox = new Gtk::HBox($false, 0); $frame = new Gtk::Frame("Priority:"); $frame->set_shadow_type("none"); $hbox->add($frame); $frame->add(make_priority_option("pri", $e->{'pri'})); $frame = new Gtk::Frame("Status:"); $frame->set_shadow_type("none"); $hbox->add($frame); $frame->add(make_status_option("status", $e->{'status'})); $frame = new Gtk::Frame("Description:"); $frame->set_shadow_type("none"); $hbox->add($frame); $entry = new Gtk::Entry(255); $entry->signal_connect("activate", sub { todolist_update($key, $entry); $window->destroy(); }); $frame->add($entry); # select the current entry (again, nice when editing) if(defined($e->{'data'})) { $entry->set_text($e->{'data'}); $entry->select_region(0, length($e->{'data'})); } $vbox->add($hbox); $hbox = new Gtk::HBox($true, 5); $button = make_button("Save"); $button->signal_connect("clicked", sub { todolist_update($key, $entry); $window->destroy(); }); $hbox->add($button); $button = make_button("Cancel"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $vbox->add($hbox); $entry->grab_focus(); $window->show_all(); } # adds / updates internal calendar data for later saving sub calendar_update { my ($year, $month, $day, $key, $entry) = @_; return unless($year && $month && $day); my $data = $entry->get_chars; unless(length($data)) { calendar_warn("No description provided for entry."); return(0); } # get the existing entry for later processing my $oe = get_calendar_entry($key); my $sy = $data{'s'}{'sy'}; my $sM = $data{'s'}{'sM'}; my $sd = $data{'s'}{'sd'}; my $sh = $data{'s'}{'sh'}; my $sm = $data{'s'}{'sm'}; my $sp = $data{'s'}{'sp'}; my $ey = $data{'s'}{'ey'}; my $eM = $data{'s'}{'eM'}; my $ed = $data{'s'}{'ed'}; my $eh = $data{'s'}{'eh'}; my $em = $data{'s'}{'em'}; my $ep = $data{'s'}{'ep'}; my $alrm = $data{'s'}{'alrm'}; my $alrm_tm = $data{'s'}{'alrm_tm'}; my $all_day = $data{'s'}{'all_day'}; if($twelve_hour == 1) { $sh += 12 if($sh < 12 && $sp eq "PM"); $eh += 12 if($eh < 12 && $ep eq "PM"); $sh -= 12 if($sh > 12 && $sp eq "AM"); $eh -= 12 if($eh > 12 && $ep eq "AM"); $sh = 0 if($sh == 12 && $sp eq "AM"); $eh = 0 if($eh == 12 && $ep eq "AM"); } if($all_day == $true) { $ey = $sy; $eM = $sM; $ed = $sd; # use pre-defined start and end hours for all day events $sh = $day_start; $eh = $day_end; $sm = 0; $em = 0; } else { # still look at input hours to see if it matches all day my $ds = $day_start; my $de = $day_end; if($sh == $ds && $eh == $de && $sm == 0 && $em == 0) { $all_day = 1; } } # for sorting $sy = sprintf("%04d", $sy); $sM = sprintf("%02d", $sM); $sd = sprintf("%02d", $sd); $sh = sprintf("%02d", $sh); $sm = sprintf("%02d", $sm); $ey = sprintf("%04d", $ey); $eM = sprintf("%02d", $eM); $ed = sprintf("%02d", $ed); $eh = sprintf("%02d", $eh); $em = sprintf("%02d", $em); if("$ey$eM$ed$eh$em" <= "$sy$sM$sd$sh$sm") { calendar_warn("Start date is after End date?"); return(0); } my $new_key = "$sy$sM$sd$sh$sm:$ey$eM$ed$eh$em:"; calendar_delete_entry($key); # new entry in the same time-frame, find an open slot if(defined($calendar_entries{$new_key . "00"})) { my $c = "00"; while(defined($calendar_entries{$new_key . $c})) { # I think 5 would probably be enough, but whatever :) if($c++ >= 99) { my $msg; $msg = "Unable to add new entry. The maximum "; $msg .= "count for the same time-frame (99) "; $msg .= "has been reached.\n\nYikes, you are "; $msg .= "busy! :P"; calendar_warn($msg); return(0); } $c = sprintf("%02d", $c); } $new_key .= sprintf("%02d", $c); } else { # no entries in this time-frame $new_key .= "00"; } $calendar_entries{$new_key}{'data'} = $data; $calendar_entries{$new_key}{'type'} = "normal"; $calendar_entries{$new_key}{'all_day'} = $all_day; $calendar_entries{$new_key}{'sy'} = $sy; $calendar_entries{$new_key}{'sM'} = $sM; $calendar_entries{$new_key}{'sd'} = $sd; $calendar_entries{$new_key}{'sh'} = $sh; $calendar_entries{$new_key}{'sm'} = $sm; $calendar_entries{$new_key}{'ey'} = $ey; $calendar_entries{$new_key}{'eM'} = $eM; $calendar_entries{$new_key}{'ed'} = $ed; $calendar_entries{$new_key}{'eh'} = $eh; $calendar_entries{$new_key}{'em'} = $em; my ($us, $ue) = get_unix_times($sy, $sM, $sd, $sh, $sm, $ey, $eM, $ed, $eh, $em); $calendar_entries{$new_key}{'us'} = $us; $calendar_entries{$new_key}{'ue'} = $ue; # if entry start time has already passed, no sense for an alarm $calendar_entries{$new_key}{'alrm'} = ($us <= time) ? 0 : $alrm; $calendar_entries{$new_key}{'alrm_tm'} = $alrm_tm; # reset the alarm for the entry, but only if the time has changed if($oe->{'sy'} != $sy || $oe->{'sM'} != $sM || $oe->{'sd'} != $sd || $oe->{'sh'} != $sh || $oe->{'sm'} != $sm) { $calendar_entries{$new_key}{'alrm'} = $alrm; } calendar_view(); calendar_save(); return(1); } # adds / updates internal todo-list data for later saving sub todolist_update { my ($key, $entry) = @_; my $data = $entry->get_chars; return unless(length($data)); my $e = get_todolist_entry($key); if($e) { # remove old entry delete $todolist_entries{$key}; } else { # no entry found, adding new $e->{'time'} = time; $e->{'slot'} = 0; } my $new_key = $data{'s'}{'pri'} . ":" . $e->{'time'} . ":"; my $c = $e->{'slot'}; # add entry in the same time-frame, find an open slot while(!get_todolist_entry($new_key . $c)) { if($c++ >= 9) { my $msg; $msg = "Unable to add new entry. The maximum count "; $msg .= "for the same time-frame (9) has been "; $msg .= "reached.\n\nYikes, you are busy! :P"; calendar_warn($msg); return; } } $new_key .= $c; $todolist_entries{$new_key} = { data => $data, status => $data{'s'}{'status'} }; todolist_view(); todolist_save(); } # display a message in a small window sub calendar_warn { my $message = $_[0]; return unless(length($message)); my $window; my $vbox; my $label; my $button; $window = new Gtk::Window("toplevel"); $window->set_title("loCal: Whoops!"); $window->border_width(5); $window->set_usize(250, 0); $window->set_policy($false, $false, $false); $vbox = new Gtk::VBox($false, 5); $window->add($vbox); $label = new Gtk::Label($message); $label->set_alignment(0.5, 0.5); $label->set_line_wrap($true); $label->set_usize(240, 0); $vbox->add($label); $button = new Gtk::Button("Close"); $button->signal_connect("clicked", sub { $window->destroy(); }); $vbox->pack_start($button, $false, $false, 0); $button->can_default($true); $button->grab_default(); $window->show_all(); } sub make_time_entry { my ($x, $h, $m) = @_; my $entry; if($twelve_hour == 1) { $h -= 12 if($h > 12); $h = 12 if($h == 0); } $data{'s'}{$x . "h"} = $h; $data{'s'}{$x . "m"} = $m; $entry = new Gtk::Entry(5); $entry->set_text(sprintf("%1d:%02d", $h, $m)); $entry->signal_connect("changed", \&time_entry_changed, $x); $entry->signal_connect("event", \&time_entry_handler, $x); $entry->set_usize(45, 0); return($entry); } # data in the time entry has changed, keep checking until there's valid data sub time_entry_changed { my ($entry, $x) = @_; my $data = $entry->get_chars; my ($h, $m) = is_valid_time($data); return unless(defined($h) && defined($m)); $data{'s'}{$x . "h"} = $h; $data{'s'}{$x . "m"} = $m; } # clean up the input, make sure there's only digits. sub time_entry_handler { my ($entry, $x, $event) = @_; return(0) unless($event->{'type'} eq "focus_change"); my $h = $data{'s'}{$x . "h"}; my $m = $data{'s'}{$x . "m"}; $entry->set_text(sprintf("%1d:%02d", $h, $m)); return(0); } # checks data from time entries to make sure they are xx:xx and valid ranges sub is_valid_time { my $data = $_[0]; return(undef, undef) unless($data =~ /:/); my ($x, $y) = split(/:/, $data); $x =~ s/\D//g; $y =~ s/\D//g; $data = $x . ":" . $y; if($data =~ m/^(\d+):(\d+)$/) { my $h = $1; my $m = $2; my $mh = 0; my $mm = 23; if($twelve_hour == 1) { $mm = 1; $mh = 12; } if(($h < $mm || $h > $mh) || ($m < 0 || $m > 59)) { return(undef, undef); } return($h, $m); } return(undef, undef); } sub make_ampm_option { my ($i, $p) = @_; my $opt; my $menu; my $menu_item; # if the option menu isn't touched when editing, the values aren't set $data{'s'}{$i} = $p; $opt = new Gtk::OptionMenu(); $menu = new Gtk::Menu(); $menu_item = make_menu_item("AM", undef, \&menu_option_changed, $i); $menu->append($menu_item); $menu_item = make_menu_item("PM", undef, \&menu_option_changed, $i); $menu->append($menu_item); $opt->set_menu($menu); $opt->set_history(1) if($p eq "PM"); return($opt); } sub make_priority_option { my ($i, $p) = @_; my $opt; my $menu; my $menu_item; # if the option menu isn't touched when editing, the values aren't set $data{'s'}{$i} = $p; $opt = new Gtk::OptionMenu(); $menu = new Gtk::Menu(); for(my $level = 1; $level < scalar(@priorities); $level++) { $menu_item = make_menu_item($priorities[$level], $level, \&menu_option_changed, $i); $menu->append($menu_item); } $opt->set_menu($menu); $opt->set_history($p-1); return($opt); } sub make_status_option { my($i, $s) = @_; my $opt; my $menu; my $menu_item; # if the option menu isn't touched when editing, the values aren't set $data{'s'}{$i} = $s; $opt = new Gtk::OptionMenu(); $menu = new Gtk::Menu(); for(my $level = 0; $level < scalar(@status); $level++) { $menu_item = make_menu_item($status[$level], $level, \&menu_option_changed, $i); $menu->append($menu_item); } $opt->set_menu($menu); $opt->set_history($s); return($opt); } sub menu_option_changed { my ($menu_item, $i, $p) = @_; $data{'s'}{$i} = $p; } sub make_menu_item { my ($name, $val, $callback, $data) = @_; my $menu_item; # for todo-list $val = $name unless(defined($val)); $menu_item = new Gtk::MenuItem($name); $menu_item->signal_connect("activate", $callback, $data, $val); $menu_item->show(); return($menu_item); } # wrapper to create a Gtk button with the focus nonsense off sub make_button { my $face = $_[0] || "(null)"; my $button; $button = new Gtk::Button($face); $button->can_focus($false); return($button); } # callback used for alarms and/or removing old entries sub calendar_entries_check { my $save = $false; my $view = $false; my $key; foreach $key (sort(keys(%calendar_entries))) { my $e = get_calendar_entry($key); my $r = calendar_entry_is_valid_alarm($e); # entry is old if($r == -1) { $save = $true; # delete old entries if($delete_old == $true) { $view = $true; calendar_delete_entry($key); } else { # disable alarm and save $calendar_entries{$key}{'alrm'} = $false; } } next unless($r == $true); # show the notice once, disable and save $calendar_entries{$key}{'alrm'} = $false; $save = $true; calendar_alarm_window($e, $key); } calendar_view_refresh() if($view == $true); calendar_save() if($save == $true); return(1); } sub calendar_entry_is_valid_alarm { my $e = $_[0]; return($false) unless($e); my $s_time = $e->{'us'}; my $e_time = $e->{'ue'}; my $c_time = time; # old entry, before last 24 hours, delete return(-1) if(($c_time - 86400) > $e_time); # old entry, within the last 24 hours return($false) if($c_time > $e_time); # alarms disabled return($false) if($use_alarm == $false); # currently running event that's already been displayed return($false) if($c_time > $s_time && $e->{'alrm'} == $false); # alarm already displayed return($false) if($e->{'alrm'} == $false); # not yet return($false) if(($c_time + ($e->{'alrm_tm'} * 60)) <= $s_time); return($true); } sub calendar_alarm_window { my ($e, $key) = @_; my $sh = $e->{'sh'}; my $sm = $e->{'sm'}; my $sp = $e->{'sp'}; my $eh = $e->{'eh'}; my $em = $e->{'em'}; my $ep = $e->{'ep'}; my $msg; my $window; my $main_vbox; my $vbox; my $hbox; my $frame; my $label; my $button; my $tooltip; $window = new Gtk::Window("toplevel"); $window->set_title("loCal: Notice"); $window->border_width(5); $window->set_usize(275, 0); $window->set_policy($false, $false, $false); my @gtk_xpm = ( '26 26 31 1', ' c none', '. c gray9', 'X c #1d1d1d', 'o c #242424', 'O c #2b2b2b', '+ c #353535', '@ c #3c3c3c', '# c #434343', '$ c #4b4b4b', '% c #525252', '& c #5b5b5b', '* c #636363', '= c #6b6b6b', '- c #747474', '; c #7b7b7b', ': c #828282', '> c #8a8a8a', ', c #949494', '< c #9a9a9a', '1 c #a3a3a3', '2 c #aaaaaa', '3 c #b3b3b3', '4 c #bcbcbc', '5 c #c3c3c3', '6 c #cccccc', '7 c #d3d3d3', '8 c gray86', '9 c #e3e3e3', '0 c #ebebeb', 'q c #f4f4f4', 'w c #f9f9f9', ' ', ' $@@# #@@$ ', ' c@OooO* *OooO@c ', ' +&ooOO$+@O#OOoo&+ ', ' c&oOOO++$$$$@@O@o&c ', ' :&&+$-::=*,>>:&&: ', ' &@;;-<<&=35187>& ', ' =@>3,=<1&;5710w0:= ', ' ;=,32<3,==4750w08; ', ' &&=&1555>&*390qq604& ', ' #::=36644=:800w05w0= ', ' *$24566674;>900qwww0:& ', ' *$>,667665;;8990ww87>* ', ' &&-;667783++3889qq63>* ', ' &&<1666777=-7780qw74>& ', ' &&25556778,:8990qq07-& ', ' *#,2;56687:,79890485$& ', ' $;,&15686>;79875>4,$ ', ' +#*;<3168<188244<>** ', ' -#$-,<;486566:231*#- ', ' *#$-<;55;>64;:,*$* ', ' *@$*:24;>43>;&@* ', ' &o#$->::;*&@+& ', ' ## #$$##$$# ## ', ' :# #: ', ' '); $window->realize(); my ($icon, $mask) = Gtk::Gdk::Pixmap->create_from_xpm_d($window->window, $window->style->white, @gtk_xpm); my $pixmap = new Gtk::Pixmap($icon, $mask); $main_vbox = new Gtk::VBox($false, 5); $window->add($main_vbox); $hbox = new Gtk::HBox($false, 5); $hbox->pack_start($pixmap, $false, $false, 0); $vbox = new Gtk::VBox($false, 5); if($twelve_hour == 1) { $sh -= 12 if($sh >= 12); $eh -= 12 if($eh >= 12); $sh = 12 if($sh == 0); $eh = 12 if($eh == 0); $sh = sprintf("%1d", $sh); $eh = sprintf("%1d", $eh); $msg = "$sh:$sm $sp - $eh:$em $ep"; } else { $msg = "$sh:$sm - $eh:$em"; } $label = new Gtk::Label($msg); $label->set_alignment(0, 0.5); $vbox->add($label); $label = new Gtk::Label($e->{'data'}); $label->set_usize(225, 0); $label->set_alignment(0, 0.5); $label->set_line_wrap($true); $vbox->add($label); $hbox->add($vbox); $main_vbox->add($hbox); $hbox = new Gtk::HBox($true, 5); $button = make_button("Remind Me Later"); $button->signal_connect("clicked", sub { $calendar_entries{$key}{'alrm'} = $true; $window->destroy(); }); $tooltip = new Gtk::Tooltips(); $tooltip->set_tip($button, "Display this notification again in 5 minutes.", ""); $hbox->add($button); $button = make_button("Close"); $button->signal_connect("clicked", sub { $window->destroy(); }); $hbox->add($button); $main_vbox->add($hbox); $window->show_all(); } # read the calendar data file sub calendar_load { open(FILE, $loCal_calendar) || return; while() { chomp; my ($key, $type, $alrm, $alrm_tm, $data) = split(/\|\|/); # make sure there's sane data next unless(defined($key) && defined($type)); next unless(length($data)); my ($sy, $sM, $sd, $sh, $sm, $ey, $eM, $ed, $eh, $em) = ($key =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2}):(\d{4})(\d{2})(\d{2})(\d{2})(\d{2}):\d{2}$/); next unless(defined($sy) && defined($ey)); $calendar_entries{$key}{'data'} = $data; $calendar_entries{$key}{'type'} = $type; $calendar_entries{$key}{'sy'} = $sy; $calendar_entries{$key}{'sM'} = $sM; $calendar_entries{$key}{'sd'} = $sd; $calendar_entries{$key}{'sh'} = $sh; $calendar_entries{$key}{'sm'} = $sm; $calendar_entries{$key}{'ey'} = $ey; $calendar_entries{$key}{'eM'} = $eM; $calendar_entries{$key}{'ed'} = $ed; $calendar_entries{$key}{'eh'} = $eh; $calendar_entries{$key}{'em'} = $em; $calendar_entries{$key}{'alrm'} = $alrm; $calendar_entries{$key}{'alrm_tm'} = ($alrm_tm < 5) ? 5 : $alrm_tm; my ($us, $ue) = get_unix_times($sy, $sM, $sd, $sh, $sm, $ey, $eM, $ed, $eh, $em); $calendar_entries{$key}{'us'} = $us; $calendar_entries{$key}{'ue'} = $ue; my $ds = $day_start; my $de = $day_end; my $all_day = $false; if($sh == $ds && $sm == 0 && $eh == $de && $em == 0) { $all_day = $true; } $calendar_entries{$key}{'all_day'} = $all_day; } close(FILE); } # save the calendar data file sub calendar_save { my $key; # doesn't matter much, but there's no need to keep an empty file if(!%calendar_entries && -f $loCal_calendar) { unlink($loCal_calendar); return; } # create loCal home, but only if saving mkdir($loCal_home, 0700) unless(-d $loCal_home); open(FILE, "> $loCal_calendar") || return; foreach $key (sort(keys(%calendar_entries))) { my $data = $calendar_entries{$key}{'data'}; my $type = $calendar_entries{$key}{'type'}; my $alrm = $calendar_entries{$key}{'alrm'}; my $alrm_tm = $calendar_entries{$key}{'alrm_tm'}; print FILE $key . "||" . $type . "||" . $alrm . "||" . $alrm_tm . "||" . $data . "\n"; } close(FILE); chmod(0600, $loCal_calendar); } sub get_unix_times { my ($sy, $sM, $sd, $sh, $sm, $ey, $eM, $ed, $eh, $em) = @_; return(undef, undef) unless(defined($sy) && defined($ey)); my $us = UnixDate($sy . $sM . $sd . $sh . ":" . $sm . ":00", "%s"); my $ue = UnixDate($ey . $eM . $ed . $eh . ":" . $em . ":00", "%s"); return($us, $ue); } # read the todo-list data file sub todolist_load { open(FILE, $loCal_todolist) || return; while() { chomp; my ($key, $data, $status) = split(/\|\|/); # $key == priority:time():slot_number # all are important for sorting when displaying the list # make sure there's sane data next unless($key =~ /^\d:\d+:\d$/); next unless(length($data)); $status = 0 if(not defined($status)); $todolist_entries{$key} = { data => $data, status => $status }; } close(FILE); } # save the todo-list data file sub todolist_save { my $key; # doesn't matter much, but there's no need to keep an empty file if(!%todolist_entries && -f $loCal_todolist) { unlink($loCal_todolist); return; } # create loCal home, but only if saving mkdir($loCal_home, 0700) unless(-d $loCal_home); open(FILE, "> $loCal_todolist") || return; foreach $key (sort(keys(%todolist_entries))) { print FILE $key . "||" . $todolist_entries{$key}{'data'} . "||" . $todolist_entries{$key}{'status'} . "\n"; } close(FILE); chmod(0600, $loCal_todolist); } sub opp { ($_[0] == 1) ? return(0) : return(1); } # stolen from Time::DaysInMonth.pm: David Muir Sharnoff sub Time::DaysInMonth::days_in { my ($year, $month) = @_; my %mltable = qw( 1 31 3 31 4 30 5 31 6 30 7 31 8 31 9 30 10 31 11 30 12 31); # Month is 1..12 return $mltable{$month+0} unless $month == 2; return 28 unless &Time::DaysInMonth::is_leap($year); return 29; } # stolen from Time::DaysInMonth.pm: David Muir Sharnoff sub Time::DaysInMonth::is_leap { my ($year) = @_; return 0 unless $year % 4 == 0; return 1 unless $year % 100 == 0; return 0 unless $year % 400 == 0; return 1; } sub window_configure_event { my $window = $_[0]; my ($window_x, $window_y) = $window->window->get_position(); my ($window_h, $window_w) = $window->window->get_size(); $config{'window_x'} = $window_x; $config{'window_y'} = $window_y; $config{'window_h'} = $window_h; $config{'window_w'} = $window_w; } sub config_load { return unless(-f $loCal_rc); open(FILE, $loCal_rc) || return; while() { chomp; # currenly only geometry stuffs if(m/(\w+)\s+=\s+(\d+)$/) { $config{$1} = $2; } } close(FILE); } sub config_save { my $config = $loCal_rc; open(FILE, "> $loCal_rc") || return; foreach my $key (sort(keys(%config))) { print FILE "$key = $config{$key}\n"; } close(FILE); } sub loCal_quit { config_save(); Gtk->exit(0); } # --- ChangeLog --------------------------------------------------------------- # # 0.01 - 12/30/2002: # initial release. # # 0.02 - 12/31/2002: # re-arranged the add/edit window. # entries in the same timeslots are no longer overwritten, instead the # end minute is incrimented by 1. ya, but whatever. :D # # 0.03 - 12/31/2002: # although entries are still added/edited using 24 hour time, they can # now be viewed in 12 hour. kindof strange, but I like it. if you don't, # just set $twelve_hour_view = 0 at the top. # when all day events are edited they will activate the check button. # fixed a problem overwriting existing entries when editing. # other minor cleanups. # # 0.04 - 12/31/2002: # updated the the add/edit panel to use a frame around the Start and End # times. # # 0.05 - 1/1/2003: # adding/editing entries has been moved to 12 hour format, seems to work # ok but it could be pretty buggy yet. whatever you do, don't look at # the code for this, you'll vomit all over yourself and I don't want to # foot the dry cleaning bill. :) the actual data is still stored in 24 # hour format. # on a related note, $twelve_hour_view is gone... # # 0.06 - 1/1/2003: # made the time spinners wrap. # added $twelve_hour, set this to 0 if you want to use 24 hour time when # viewing and adding entries, default is 12 hour. # fixed a time problem when the add window is initially displayed, if the # start hour is 12 and end hour is 13 (1pm), end hour being set to 2pm. # made the max value for hour (in 12 hour) 12, had this in the wrong spot. # # 0.07 - 1/1/2003: # updated window titles. # the calendar data file will be removed if the last entry has been deleted. # re-worked the same time-frame entries, there's now slots for each calendar # entry, a max of 99 slots for the same time-frame has been setup. this # means any existing calendar data is not compatible with this version. # clear day button added on the day view window. # # 0.08 - 1/2/2003: # end hour am/pm option menu fixed to reflect the correct data. # a clist is used to display the current entries for the selected day # on the main calendar window instead of using the seperate day view. # fixed a few problems due to the 24-12 hour conversion. # # 0.09 - 1/3/2003: # minor adjustment to the padding around the main window buttons. # updated the day view panel to not have the Viewing MM/DD/YYYY label # scrollable. # added a little statusbar under the calendar to display the current # date and time. the various lables and titles have all been updated to # reflect the same date format. # # 0.10 - 1/3/2003: # updated the calendar data file format again, calendar data files created # with versions prior to 0.10 won't work with this release. the data is # stored in a rather simple manner, so this may change in the future if # I ever want do anything really useful with this. :P the current format # would allow for recurring entries as well as entries that span many # days. woo! # major overhaul of the calendar data storage & retrieval system. # re-organized the 24-12 hour conversion a bit, should be less problematic. # updated the 24 hour view to show times as 1530 instead of 15:30. # # 0.11 - 1/4/2003: # re-arranged the main panel buttons. # made the current date/time statusbar into a clickable button that will # reset the calendar view to the current date. # other minor cleanups. # # 0.12 - 1/4/2003: # addition of todo-list (see toggle button above calendar). this is kindof # messy, mostly duplicated and modified the calendar functions. # # 0.13 - 1/5/2003: # reverted 24 hour view back to 15:30 vs 1530. :P # updated the todo-list file format and cleaned up the sorting for the # todo-list view. # other minor cleanups. # # 0.14 - 1/6/2003: # brought back the clist titles and updated the todo-list view to include # the time the entry was added. # updated get_todolist_entry() to return undef for entries not found and # todolist_update() to handle additional an additional variable (slot). # added date and time strftime format variables for easier customization. # removed the duplicate calendar_del(), must have yy'd too many lines. :P # added $day_start and $day_end variables which are used for creating all # day events. these can be commented completely or cutomized. # # 0.15 - 1/6/2003: # added calendar alarms/event notifications, set $use_alarm to 0 to disable. # this is more on the experimental side, so don't trust it too blindly. # format of the calendar data file has changed yet again (for alarms). # # 0.16 - 1/6/2003: # updated the add/edit window to not display the alarm section if alarms # have been disabled. # # 0.17 - 1/7/2003: # the time spinners/widgets will be disabled if the All Day Event button # is checked. same goes for the alarm spinner (if the alarm checkbox is # not checked). # # 0.18 - 1/8/2003: # added multi-day events; main calendar day view will display < or > if it # the entry begins prior or ends after the current day. double click (or # click edit) the selected entry to see what the actual start time is if # you are that confused about your calendar. :P a good portion of this # is pretty messy and isn't yet optimized. much work needed here. # re-arranged the add/edit window; start/end sections are stacked on top # of eachother vs side-by-side (window was too wide) and the time # spinners have been converted to regular text entry boxes. # these are some pretty major updates, watch out fer bugs! # # 0.19 - 1/8/2003: # add/edit time entry validation updates; now works when using the mouse or # keyboard to navigate. invalid characters are stripped out, r4:15 becomes # 4:15. # # 0.20 - 1/9/2003: # did some optimization on calendar_month_changed(), which marks the various # days on the calendar that have entries. # added an option ($delete_old) to remove old entries. this is enabled by # default and is suggested if you have many calendar entries. # fixed the annoying behavior with the time entry boxes, now it works more # like the GtkAdjustment/Spinner widget. # changed the alarm check from once every 60 seconds to 5 minutes. # added a 'Remind Me Later' button to the notification window that will # show the same reminder again in 5 minutes. this button will only be # displayed if $show_alarm_once is enabled. note: if you close loCal # after clicking this button, you will NOT see the notification again. # fixed a problem when updating multi-day events to all day. # other minor cleanups. # # 0.21 - 1/10/2003: # old calendar entries will now be removed if use of alarms is disabled. # added a little alarm clock icon to the notification window. :) # other minor cleanups. # # 0.22 - 1/11/2003: # only entries > 24 hours old are removed, so the current days items will # still be available after they have passed. # other cleanups. # # 0.23 - 1/12/2003: # removed $show_alarm_once, use the 'Remind Me Later' button instead. # chmod 600 calendar and todolist files. # # 0.24 - 2/12/2003: # minor update to allow the start date to be set when adding or editing # entries (good for moving meetings from one day to the next). could # use some polishing, but it's alright... # moved configuration from ~/ to ~/.loCal, be sure to move your calendar # and/or todolist files to this directory. # # 0.25 - 2/15/2003: # list titles will no longer scroll. # # 0.26 - 3/12/2003: # allow resize of the main window. # added config loading/saving for window geometry. # # 0.27 - 4/18/2003: # status patch for todo-list, updated sorting - MrBawb # # --- ChangeLog ---------------------------------------------------------------