PageRenderTime 669ms CodeModel.GetById 23ms RepoModel.GetById 18ms app.codeStats 1ms

/tktimex

https://github.com/gitpan/Timex
Perl | 4800 lines | 4435 code | 290 blank | 75 comment | 412 complexity | be8a40721fbb4ea810af4c6fcad77528 MD5 | raw file
  1. #!/usr/bin/perl
  2. # -*- perl -*-
  3. #
  4. # $Id: tktimex,v 6.24 2005/05/03 19:32:27 eserte Exp $
  5. #
  6. # Author: Slaven Rezic
  7. # Copyright: see in subroutine show_copyright or the Help/Copyright menu entry
  8. # (it's basically a BSD-styled copyright)
  9. #
  10. # Mail: mailto:eserte@users.sourceforge.net
  11. # WWW: http://ptktools.sourceforge.net/
  12. #
  13. #use blib qw(/home/e/eserte/src/CPAN/Tk-Date);#XXXXXXXXXXXXXXXX
  14. #use blib qw(/home/e/eserte/src/perl/Devel-SRT);
  15. #use Devel::SRT;
  16. #BEGIN { eval q{ use utf8 } }
  17. BEGIN {
  18. $Devel::Trace::TRACE = 0;
  19. sub state_change { }
  20. state_change("before Tk");
  21. }
  22. use Tk;
  23. BEGIN {
  24. state_change("after Tk");
  25. }
  26. use Tk::ErrorDialog;
  27. BEGIN {
  28. if (!eval '
  29. use blib "/home/e/eserte/src/perl/Msg";
  30. use Msg;
  31. 1;
  32. ') {
  33. warn $@ if $@ and $ENV{USER} and $ENV{USER} =~ /eserte/;
  34. eval 'sub M ($) { $_[0] }';
  35. eval 'sub Mfmt { sprintf(shift, @_) }';
  36. }
  37. }
  38. eval '
  39. use lib "/home/e/eserte/lib/perl";
  40. use Tk::App::Reloader;
  41. $Tk::App::Reloader::VERBOSE = 1;
  42. '; warn $@ if $@ and $ENV{USER} and $ENV{USER} =~ /eserte/;
  43. state_change("all use's processed...");
  44. ######################################################################
  45. package Tk::Wm;
  46. sub Popup
  47. {
  48. my $w = shift;
  49. $w->withdraw; # force invisible update
  50. $w->configure(@_) if @_;
  51. $w->idletasks;
  52. my ($mw,$mh) = ($w->reqwidth,$w->reqheight);
  53. my ($rx,$ry,$rw,$rh) = (0,0,0,0);
  54. my $base = $w->cget('-popover');
  55. my $outside = 0;
  56. if (defined $base)
  57. {
  58. if ($base eq 'cursor')
  59. {
  60. ($rx,$ry) = $w->pointerxy;
  61. }
  62. else
  63. {
  64. $rx = $base->rootx;
  65. $ry = $base->rooty;
  66. $rw = $base->Width;
  67. $rh = $base->Height;
  68. }
  69. }
  70. else
  71. {
  72. my $sc = ($w->parent) ? $w->parent->toplevel : $w;
  73. $rx = -$sc->vrootx;
  74. $ry = -$sc->vrooty;
  75. $rw = $w->screenwidth;
  76. $rh = $w->screenheight;
  77. }
  78. my ($X,$Y) = AnchorAdjust($w->cget('-overanchor'),$rx,$ry,$rw,$rh);
  79. ($X,$Y) = AnchorAdjust($w->cget('-popanchor'),$X,$Y,-$mw,-$mh);
  80. my ($sh,$sw) = ($w->screenheight, $w->screenwidth);
  81. $mw += 6; $mh += 28; # XXX for window manager frame
  82. if ($X + $mw > $sw) { $X = $sw - $mw }
  83. if ($X < 0) { $X = 0 }
  84. if ($Y + $mh > $sh) { $Y = $sh - $mh }
  85. if ($Y < 0) { $Y = 0 }
  86. $w->deiconify;
  87. $w->Post($X,$Y);
  88. $w->waitVisibility;
  89. }
  90. ######################################################################
  91. package Tk::MyHList;
  92. @Tk::MyHList::ISA = qw(Tk::HList);
  93. Construct Tk::Widget 'MyHList';
  94. # Hack to prevent the selection to disappear if clicking on empty hlist space
  95. sub Button1 {
  96. my $w = shift;
  97. my($orig_sel) = $w->selectionGet;
  98. my $r = $w->SUPER::Button1(@_);
  99. if (!$w->selectionGet && $orig_sel) {
  100. $w->selectionSet($orig_sel);
  101. }
  102. $r;
  103. }
  104. sub ButtonRelease_1
  105. {
  106. my $w = shift;
  107. my $Ev = $w->XEvent;
  108. $w->CancelRepeat
  109. if($w->cget('-selectmode') ne 'dragdrop');
  110. main::MyButtonRelease1($w, $Ev);
  111. }
  112. ######################################################################
  113. package Tk::MyTree;
  114. use base qw(Tk::Tree);
  115. Construct Tk::Widget 'MyTree';
  116. sub Button1 { shift->Tk::MyHList::Button1(@_) }
  117. sub ButtonRelease_1 { shift->Tk::MyHList::ButtonRelease_1(@_) }
  118. ######################################################################
  119. package main;
  120. ##use Tk::HList;
  121. ##use Tk::Tree;
  122. BEGIN { state_change("after Tk::HList"); }
  123. use File::Basename;
  124. use FindBin;
  125. use lib ("$FindBin::RealBin");
  126. BEGIN {
  127. unshift @INC, "$_/Timex" for reverse @INC;
  128. }
  129. eval { require Tk::UnderlineAll unless $Tk::VERSION eq 803.023 };
  130. BEGIN { state_change("before Timex::Project"); }
  131. use Timex::Project;
  132. BEGIN { state_change("after Timex::Project"); }
  133. use strict;
  134. use vars qw($VERSION);
  135. #XXX broken in perl5.6.0+Linux: floating numbers are interpreted as integers?!
  136. #use locale; # for sort, broken in older FreeBSD
  137. BEGIN { state_change("after use locale"); }
  138. # enable DnD
  139. use Tk::DropSite;
  140. BEGIN { state_change("after Tk::DropSite"); }
  141. use File::Spec qw();
  142. $VERSION = sprintf("%d.%02d", q$Revision: 6.24 $ =~ /(\d+)\.(\d+)/);
  143. use vars qw($os);
  144. $os = ($^O eq 'MSWin32' || $^O eq 'os2' ? 'win' : 'unix');
  145. use vars qw($can_lock $lock_is_strict $file_writeable);
  146. $can_lock = ($^O ne 'MSWin32'); # gefährlich für win...
  147. $lock_is_strict = ($os eq 'win');
  148. if ($Tk::VERSION <= 402.002) {
  149. Tk::HList->EnterMethods("Tk/HList.pm", qw(header));
  150. }
  151. use vars qw($root $templates_root);
  152. $root = new Timex::Project;
  153. use vars qw($utmp
  154. $quit_dialog $title
  155. $current_project $is_opened
  156. $start_session_time $time_after $time_update
  157. $autosave_after @nowtime $today_time
  158. $status_text $project_frame $status_edit
  159. $separator $undo_register
  160. $last_projects $max_last_projects
  161. $username $realname $home
  162. $inner_fg $inner_bg
  163. $status_browse_entry
  164. %history %rcs_cache @rcs_cache
  165. $ctrl_s $sbside $use_enterprise
  166. $load_merge_filename $load_menu
  167. $p_itemtype %icons
  168. $old_search_regex $initial_search_direction
  169. );
  170. $title = "tktimex $VERSION";
  171. $current_project = undef;
  172. $start_session_time = time;
  173. $time_update = 0;
  174. @nowtime = localtime;
  175. $today_time = time - $nowtime[0] - $nowtime[1]*60 - $nowtime[2]*60*60;
  176. # There are two forms of separators: for intern store in HList
  177. # use $separator, for human-readable output use "/".
  178. $separator = 'Ś';
  179. # XXX iso8859-1 support with newest Tk's is buggy
  180. if ($Tk::VERSION >= 803) {
  181. $separator = '|';
  182. }
  183. $status_edit = 0;
  184. $max_last_projects = 4;
  185. $inner_fg = "black";
  186. $inner_bg = "white";
  187. $ctrl_s = ($os eq 'win' ? 'Ctrl-' : 'C-');
  188. $sbside = ($os eq 'win' ? 'e' : 'w');
  189. $use_enterprise = 0;
  190. $p_itemtype = 'imagetext';
  191. $home = get_home_dir();
  192. eval {
  193. require Timex::Utmp;
  194. $utmp = new Timex::Utmp;
  195. $utmp->init;
  196. }; warn $@ if $@;
  197. use vars qw($date_require);
  198. $date_require = <<'EOF';
  199. require Tk::Date;
  200. Tk::Date->VERSION(0.27);
  201. if ($Tk::Date::VERSION >= 0.30) {
  202. ($inner_bg_opt, $inner_fg_opt) = ('-innerbg', '-innerfg');
  203. }
  204. if ($Tk::Date::VERSION >= 0.33) {
  205. %date_args = (-allarrows => 1);
  206. }
  207. $has_date = 1;
  208. EOF
  209. state_change("before Getopt definition");
  210. use vars qw($max_enterprise_file_list @all_domains $options @opttable);
  211. $max_enterprise_file_list = 4;
  212. $options = {};
  213. @opttable =
  214. (M"General",
  215. ['file|f', '=s', get_home_dir() . "/.timex.pj1",
  216. 'subtype' => 'file',
  217. 'longhelp' => M"Default project file to load on startup",
  218. 'callback-interactive' => sub { load_file(0) },
  219. ],
  220. ['offlinefile', '=s', undef, 'subtype' => 'file',
  221. 'longhelp' => M"Fallback project file for offline operation",
  222. ],
  223. ['mergedir', '=s', undef, 'subtype' => 'file', 'nogui' => 1],
  224. ['lock', '!', 1,
  225. 'longhelp' => M"Set to false if you don't want file locking."],
  226. ['one-instance', '!', 0,
  227. 'longhelp' => M"Exit application if there is already another instance running"],
  228. ['autosave', '!', 1,
  229. 'callback' => \&toggle_autosave, 'alias' => ['as'],
  230. 'longhelp' => M"Autosave is recommended!"],
  231. ['update', '=i', 60*10,
  232. 'longhelp' => M"Autosave interval in seconds."],
  233. ['oneday-immediately', '!', 1,
  234. 'longhelp' => M"Daily details: immediate update if changing date"],
  235. ['geometry', '=s', "500x230",
  236. 'subtype' => 'geometry',
  237. 'longhelp' => M"Size of window on startup"], # XXX
  238. ['iconified', '!', 0,
  239. 'longhelp' => M"Open application iconfied"],
  240. ['securesave', '!', 0,
  241. 'longhelp' => M"Saving data also in a Data::Dumper format. Not really necessary anymore."],
  242. ['plugins', '=s', undef,
  243. 'longhelp' => M"A comma-separated list of initial plugins to load."],
  244. ['username', '=s', $ENV{USERNAME},
  245. 'longhelp' => M"A unix-like short username"],
  246. ['realname', '=s', undef,
  247. 'longhelp' => M"The user's real name"],
  248. M"Enterprise",
  249. ['enterpriseprojects', "=s", undef, 'subtype' => 'file',
  250. 'longhelp' => M"File with enterprise-wide list of projects"],
  251. ['enterprisedefaults', "=s", undef, 'subtype' => 'file',
  252. 'longhelp' => M"File with enterprise-wide configuration settings"],
  253. (map { ["enterprisefile$_", "=s", undef, 'subtype' => 'file',
  254. 'longhelp' => M"Default enterprise timex file"]
  255. } (1 .. $max_enterprise_file_list)),
  256. M"Projects",
  257. ['dateformat', '=s', 'h',
  258. 'choices' => ['d', 'h', 'hs', 'frac d', 'frac h'],
  259. 'strict' => 1,
  260. 'callback' => \&set_dateformat,
  261. 'alias' => ['df'],
  262. 'longhelp' => M"Format of time display",
  263. ],
  264. ['day8', '!', 1, 'callback' => \&toggle_time_arbeitstag,
  265. "longhelp" => M"If set: a day should be treated as 8 hours."],
  266. ['archived', '!', 0, 'callback' => \&toggle_show_archived,
  267. "longhelp" => M"Show archived projects too.",
  268. ],
  269. ['onlytop', '!', 0, 'callback' => \&toggle_show_archived,
  270. "longhelp" => M"Do not show subprojects.",
  271. ],
  272. ['domain', '=s', undef, 'callback' => \&toggle_show_archived,
  273. "longhelp" => M"Restrict projects to a single domain only.",
  274. 'choices' => \@all_domains,
  275. #'strict' => 1, # geht leider nicht...
  276. ],
  277. ['sort', '=s', 'name',
  278. 'choices' => ['', 'nothing', 'name', 'time'],
  279. 'strict' => 1,
  280. 'callback' => sub { insert_all() },
  281. ],
  282. 'Appearance',
  283. ['interface', '=s', 'all',
  284. 'choices' => ['medium', 'small'],
  285. 'strict' => 1,
  286. 'longhelp' => M"Enable/disable menus and buttons.
  287. All: show all menus and buttons.
  288. Small: show only a minimal set of menus and buttons, no statistics available
  289. Medium: only limited project manipulation possible"],
  290. ['busyind', '!', 0,
  291. 'longhelp' => M"Show a busy indicator if a project is running"],
  292. ['autoscroll', '=s', 'none',
  293. 'choices' => ['slow', 'normal', 'fast'],
  294. 'strict' => 1,
  295. 'longhelp' => M("Autoscrolling is not available on all systems.\n" .
  296. "Changes are effective on restart.")
  297. ],
  298. ['hourlyrate', '=f', 0,
  299. 'callback' => sub { update_costs_option(1) },
  300. 'longhelp' => M"Hourly rate for work.",
  301. ],
  302. ['currency', '=s', "EUR",
  303. 'choices' => ['EUR', 'USD'],
  304. 'callback' => sub { update_costs_option(1) },
  305. 'longhelp' => M"Currency for hourlyrate option.",
  306. ],
  307. ['tree', '!', 1,
  308. 'longhelp' => M"Use tree representation"],
  309. );
  310. {
  311. # save x11 options (except -geometry)
  312. my $geometry;
  313. for(my $i = 0; $i <= $#ARGV; $i++) {
  314. if ($ARGV[$i] eq '-geometry' && $i < $#ARGV) {
  315. $geometry = $ARGV[$i+1];
  316. splice @ARGV, $i, 2;
  317. $i--;
  318. }
  319. }
  320. Tk::CmdLine::SetArguments();
  321. if (defined $geometry) {
  322. push @ARGV, -geometry => $geometry;
  323. }
  324. }
  325. use vars qw($opt);
  326. eval {
  327. state_change("before require Tk::Getopt");
  328. require Tk::Getopt;
  329. Tk::Getopt->VERSION(0.49);
  330. state_change("after require Tk::Getopt");
  331. };
  332. if ($@) {
  333. warn M"No Tk::Getopt --- falling back to Getopt::Long\n";
  334. require Getopt::Long;
  335. my @getopt;
  336. push @getopt, $options;
  337. foreach (@opttable) {
  338. if (ref $_ eq 'ARRAY') {
  339. $options->{$_->[0]} = $_->[2] if defined $_->[2];
  340. push @getopt, $_->[0] . $_->[1];
  341. }
  342. }
  343. die M"Usage?" if !Getopt::Long::GetOptions(@getopt);
  344. if ($options->{'enterprisedefaults'} and
  345. -r $options->{'enterprisedefaults'}) {
  346. standalone_message_box
  347. (-message => M("The option -enterprisedefaults does not work
  348. without the perl module Tk::Getopt
  349. Please install this module from CPAN.\n"));
  350. }
  351. } else {
  352. state_change("Tk::Getopt checkpoint 1");
  353. $opt = new Tk::Getopt(-opttable => \@opttable,
  354. -options => $options,
  355. -filename => File::Spec->catfile($home, ".tktimexrc"),
  356. );
  357. state_change("Tk::Getopt checkpoint 2");
  358. $opt->set_defaults;
  359. state_change("Tk::Getopt checkpoint 3");
  360. $opt->load_options;
  361. die $opt->usage if !$opt->get_options;
  362. if ($options->{'enterprisedefaults'} and -r $options->{'enterprisedefaults'}) {
  363. $opt->load_options($options->{'enterprisedefaults'});
  364. $use_enterprise++;
  365. }
  366. require Getopt::Long; state_change("Tk::Getopt checkpoint 4");
  367. die $opt->usage if !$opt->get_options;
  368. state_change("Tk::Getopt checkpoint 5");
  369. }
  370. if (@ARGV) {
  371. $options->{'file'} = shift @ARGV;
  372. }
  373. $username = get_user_name();
  374. $realname = get_real_name();
  375. if ($use_enterprise) {
  376. $options->{'file'} =~ s/\@USER\@/$username/g;
  377. }
  378. state_change("checkpoint 1");
  379. if ($options->{'one-instance'} and tktimex_running()) {
  380. require Tk::Dialog;
  381. my $top = tkinit;
  382. $top->withdraw;
  383. $top->Dialog
  384. (-title => M"Error",
  385. -text => M("Another tktimex instance is already running.
  386. Start tktimex with the option -noone-instance, if you
  387. want really two instances of this program running.\n"),
  388. -popover => 'cursor')->Show;
  389. exit;
  390. }
  391. use vars qw($m_if $s_if);
  392. $m_if = $options->{'interface'} eq 'medium';
  393. $s_if = $options->{'interface'} eq 'small';
  394. use vars qw($top);
  395. $top = new MainWindow;
  396. Tk::App::Reloader::shortcut() if defined &Tk::App::Reloader::shortcut;
  397. $top->protocol('WM_DELETE_WINDOW', sub { quit_program() });
  398. $top->protocol('WM_SAVE_YOURSELF',
  399. sub { save_sos();
  400. # XXX andere Optionen mit speichern (?)
  401. $top->command("$^X $0 $options->{'file'}");
  402. $top->destroy;
  403. });
  404. # SIGHUP is not defined on Windows
  405. eval {
  406. local $^W = undef;
  407. $SIG{'HUP'} = sub { save_sos(); };
  408. };
  409. if ($options->{'iconified'}) {
  410. $top->iconify;
  411. }
  412. $top->title($title);
  413. $top->geometry($options->{'geometry'}) if $options->{'geometry'};
  414. eval {
  415. my $icon = $top->Pixmap(-file => Tk::findINC("Timex/mini-clock.xpm") ||
  416. "$FindBin::RealBin/Timex/mini-clock.xpm");
  417. if ($icon) {
  418. $top->Icon(-image => $icon);
  419. }
  420. }; warn $@ if $@;
  421. $top->bind("<Pause>" => sub {
  422. eval {
  423. require Tk::WidgetDump;
  424. $top->WidgetDump;
  425. }; warn $@ if $@;
  426. require Config;
  427. my $perldir = $Config::Config{'scriptdir'};
  428. require "$perldir/ptksh";
  429. });
  430. state_change("checkpoint 2");
  431. use vars qw($is_archiv $east %hl_entry $new_in_merge $changed_in_merge
  432. $weekday_style $holiday_style);
  433. use vars qw($balloon);
  434. if ($Tk::VERSION >= 800.005) {
  435. require Tk::ItemStyle; # erst ab 800.005
  436. $is_archiv = $top->ItemStyle($p_itemtype, -foreground => 'red',
  437. -background => $inner_bg);
  438. $east = $top->ItemStyle('text', -anchor => 'e',
  439. -background => $inner_bg,
  440. -foreground => $inner_fg);
  441. $hl_entry{"red"} = $top->ItemStyle($p_itemtype, -foreground => 'red');
  442. $hl_entry{"blue"} = $top->ItemStyle($p_itemtype, -foreground => 'blue');
  443. $new_in_merge = $top->ItemStyle($p_itemtype, -foreground => 'green3',
  444. -background => $inner_bg);
  445. $changed_in_merge = $top->ItemStyle($p_itemtype, -foreground => 'blue',
  446. -background => $inner_bg);
  447. $weekday_style = $top->ItemStyle("text", -anchor => "e", -background => $inner_bg);
  448. $holiday_style = $top->ItemStyle("text", -anchor => "e", -foreground => "red", -background => $inner_bg);
  449. # altes Balloon und HList vertragen sich nicht miteinander
  450. require Tk::Balloon;
  451. $balloon = $top->Balloon;
  452. }
  453. state_change("menu begin");
  454. use vars qw($menu_frame $mb_file $mb_file_menu $mb_export
  455. $mb_project $mb_show_curr_project_index
  456. $mb_project_menu $mb_options $mb_options_menu
  457. $dateformat_menu $mb_help);
  458. $menu_frame = $top->Frame(-relief => 'raised',
  459. -borderwidth => 2);
  460. $mb_file = $menu_frame->Menubutton(-text => M"File")->pack(-side => 'left');
  461. state_change("first menubutton loaded");
  462. $mb_file->command(-label => M"Load",
  463. -command => sub { load_file(1) })
  464. unless $s_if;
  465. if (!$s_if) {
  466. my $show_it = 0;
  467. for my $i (1 .. $max_enterprise_file_list) {
  468. if (defined $options->{"enterprisefile$i"} &&
  469. $options->{"enterprisefile$i"} ne "") {
  470. $show_it++;
  471. last;
  472. }
  473. }
  474. if ($show_it) {
  475. $mb_file->cascade(-label => M"Load ...");
  476. my $mb_load_menu = $mb_file->cget(-menu);
  477. my $mb_load = $mb_load_menu->Menu;
  478. $mb_file->entryconfigure('last', -menu => $mb_load);
  479. for my $i (1 .. $max_enterprise_file_list) {
  480. if (defined $options->{"enterprisefile$i"} &&
  481. $options->{"enterprisefile$i"} ne "") {
  482. my $f = $options->{"enterprisefile$i"};
  483. $mb_load->command(-label => basename($f),
  484. -command => sub { load_file(0, $f) });
  485. }
  486. }
  487. if (defined $options->{'file'} && $options->{'file'} ne '') {
  488. $mb_load->command
  489. (-label => basename($options->{'file'}),
  490. -command => sub { load_file(0, $options->{'file'}) });
  491. }
  492. }
  493. }
  494. $mb_file->command(-label => M"Save",
  495. -command => \&save_file);
  496. $mb_file->cascade(-label => M"Export");
  497. $mb_file_menu = $mb_file->cget(-menu);
  498. $mb_export = $mb_file_menu->Menu;
  499. $mb_file->entryconfigure('last', -menu => $mb_export);
  500. $mb_file->entryconfigure('last', -state => 'disabled') if $s_if || $m_if;
  501. $mb_export->command(-label => M"Save as ...",
  502. -command => \&save_as_file);
  503. $mb_export->command(-label => M"Save skeleton",
  504. -command => \&save_skeleton);
  505. $mb_export->command(-label => M"Save subproject",
  506. -command => \&save_subproject);
  507. $mb_export->command(-label => M"Save XML",
  508. -command => \&save_xml);
  509. $mb_export->command(-label => M"Save Excel",
  510. -command => sub {
  511. require Timex::ExcelExport;
  512. Timex::ExcelExport::save_dialog
  513. ($top, $root,
  514. -hourlyrate => $options->{'hourlyrate'},
  515. );
  516. });
  517. $mb_export->command(-label => M"Dump",
  518. -command => \&dump_data) unless $s_if || $m_if;
  519. $mb_file->command(-label => M"Merge",
  520. -command => \&merge_file) unless $s_if || $m_if;
  521. $mb_file->command(-label => M"Update enterprise projects",
  522. -command => \&update_enterprise_projects);
  523. $mb_file->command(-label => M"Quit",
  524. -command => sub { quit_program() });
  525. $mb_file->separator;
  526. $mb_project = $menu_frame->Menubutton(-text => M"Project"
  527. )->pack(-side => 'left');
  528. $mb_project->command(-label => M"New",
  529. -command => sub { new_project() },
  530. )
  531. unless $s_if || $m_if;
  532. # strange: -menu is only active if there is already a menu item
  533. $mb_project_menu = $mb_project->cget(-menu);
  534. $mb_project->command(-label => M"New from template",
  535. -command => \&new_project_from_template)
  536. unless $s_if || $m_if;
  537. $mb_project->command(-label => M"New subproject",
  538. -command => sub { new_sub_project(get_sel_entry()) })
  539. unless $s_if || $m_if;
  540. $mb_project->command(-label => M"Delete",
  541. -command => \&delete_project)
  542. unless $s_if || $m_if;
  543. ## Menü ist zu überladen...
  544. #$mb_project->command(-label => M"Pause",
  545. # -command => \&pause_or_cont);
  546. $mb_project->command(-label => M"Undo",
  547. -command => \&undo);
  548. $mb_project->command(-label => M"Search",
  549. -command => sub { tk_search_project(+1) },
  550. -accelerator => $ctrl_s . 's');
  551. $top->bind('<Control-s>' => sub { tk_search_project(+1) });
  552. $top->bind('<Key-slash>' => sub { tk_search_project(+1) });
  553. $top->bind('<Key-question>' => sub { tk_search_project(-1) });
  554. $top->bind('<Key-n>' => sub { search_project($old_search_regex, +1) });
  555. $top->bind('<Key-N>' => sub { search_project($old_search_regex, -1) });
  556. $mb_project->command(-label => M"Show current",
  557. -state => "disabled",
  558. -command => sub {
  559. if ($current_project) {
  560. $project_frame->see(make_path($current_project));
  561. }
  562. });
  563. $mb_show_curr_project_index = $mb_project_menu->index("last");
  564. $mb_project->command(-label => M"Continue last",
  565. -command => \&cont_last);
  566. $mb_project->command(-label => M"Attributes",
  567. -command => sub { show_attributes(undef) })
  568. unless $s_if;
  569. $mb_project->command(-label => M"Intervals",
  570. -command => sub { show_intervals($top, undef) },
  571. -accelerator => $ctrl_s .'i')
  572. unless $s_if;
  573. $top->bind('<Control-i>' => sub { show_intervals($top, undef) })
  574. unless $s_if;
  575. ## Menü ist zu überladen...
  576. #$mb_project->command(-label => M"Note",
  577. # -command => sub { show_note($top) });
  578. $mb_project->separator
  579. unless $s_if;
  580. $mb_project->command(-label => M"Working time",
  581. -command => \&working_time)
  582. unless $s_if;
  583. $mb_project->command(-label => M"Daily details",
  584. -command => \&show_one_day,
  585. -accelerator => $ctrl_s . 'd')
  586. unless $s_if;
  587. $top->bind('<Control-d>' => \&show_one_day)
  588. unless $s_if;
  589. $mb_options = $menu_frame->Menubutton(-text => M"Options"
  590. )->pack(-side => 'left');
  591. $mb_options->checkbutton(-label => M"Autosave",
  592. -command => \&toggle_autosave,
  593. -variable => \$options->{'autosave'},
  594. )
  595. unless $s_if || $m_if;
  596. $mb_options->cascade(-label => M"Dateformat");
  597. $mb_options_menu = $mb_options->cget(-menu);
  598. $dateformat_menu = $mb_options_menu->Menu;
  599. $mb_options->entryconfigure(M"Dateformat", -menu => $dateformat_menu);
  600. $mb_options->entryconfigure(M"Dateformat", -state => 'disabled')
  601. if $s_if || $m_if;
  602. $mb_options->checkbutton(-label => M"day = 8h",
  603. -command => \&toggle_time_arbeitstag,
  604. -variable => \$options->{'day8'},
  605. )
  606. unless $s_if || $m_if;
  607. $mb_options->checkbutton(-label => M"Show archived",
  608. -command => \&toggle_show_archived,
  609. -variable => \$options->{'archived'},
  610. )
  611. unless $s_if || $m_if;
  612. $mb_options->checkbutton(-label => M"Show only top",
  613. -command => \&toggle_show_only_top,
  614. -variable => \$options->{'onlytop'},
  615. )
  616. unless $s_if || $m_if;
  617. if ($options->{'tree'}) {
  618. $mb_options->separator
  619. unless $s_if || $m_if;
  620. $mb_options->command
  621. (-label => M"Open all",
  622. -command => sub { traverse_entries(sub {
  623. $project_frame->open($_[0])
  624. }) }
  625. );
  626. $mb_options->command
  627. (-label => M"Close all",
  628. -command => sub { traverse_entries(sub {
  629. $project_frame->close($_[0])
  630. }) }
  631. );
  632. }
  633. if (defined $opt) {
  634. $mb_options->separator
  635. unless $s_if || $m_if;
  636. $mb_options->command(-label => M"Option editor",
  637. -command => sub { $opt->option_editor($top, -buttons => ['oksave', 'cancel']) })
  638. unless $s_if || $m_if;
  639. }
  640. foreach my $def ([M"H:M:S" => 'hs'],
  641. [M"H:M" => 'h'],
  642. [M"d H:M" => 'd'],
  643. [M"Frac H" => 'frac h'],
  644. [M"Frac d" => 'frac d'],
  645. ) {
  646. $dateformat_menu->radiobutton(-label => $def->[0],
  647. -command => \&set_dateformat,
  648. -value => $def->[1],
  649. -variable => \$options->{'dateformat'},
  650. );
  651. }
  652. $mb_help = $menu_frame->Menubutton(-text => M"Help"
  653. )->pack(-side => 'left');
  654. $mb_help->command(-label => M"About",
  655. -command => \&show_about);
  656. $mb_help->command(-label => M"Copyright",
  657. -command => \&show_copyright);
  658. $mb_help->command
  659. (-label => M"Index",
  660. -command => sub {
  661. eval {
  662. require Tk::Pod;
  663. Tk::Pod->Dir($FindBin::Bin);
  664. $top->Pod(-file => "$FindBin::Script",
  665. -title => "tktimex",
  666. );
  667. };
  668. $status_text->configure(-text => substr($@, 0, 40) . "...") if $@;
  669. });
  670. state_change("menu done");
  671. use vars qw($save_check $mod_watch $mod_sub);
  672. # Aus mir völlig unerklärlichen Gründen muß sich mod_sub außerhalb
  673. # des evals befinden (perl5.00404)
  674. $mod_sub = sub {
  675. my($self, $newval) = @_;
  676. if ($newval) {
  677. $save_check->configure(-bg => 'red');
  678. } else {
  679. $save_check->configure(-bg => 'green');
  680. }
  681. $self->Store($newval) if $self;
  682. };
  683. eval {
  684. die;
  685. require Tie::Watch;
  686. # earlier versions used Delete instad of Unwatch:
  687. Tie::Watch->VERSION(0.99);
  688. $save_check = $menu_frame->Label(-padx => 4, -relief => 'raised');
  689. $mod_watch = Tie::Watch->new(-variable => \$root->{'modified'},
  690. -store => $mod_sub,
  691. );
  692. $mod_sub->();
  693. };
  694. if (!$mod_watch || $@) {
  695. $save_check = $menu_frame->Checkbutton
  696. (-variable => \$root->{'modified'},
  697. ($os ne 'win' ? (-selectcolor => "red") : ()),
  698. -highlightthickness => 0,
  699. -padx => 0, -pady => 0,
  700. #-font => "times 5",
  701. );
  702. $save_check->bindtags([]);
  703. }
  704. $save_check->pack(-side => 'right');
  705. $balloon->attach($save_check, -msg => M"Timex data modified indicator")
  706. if $balloon;
  707. use vars qw($pause_cont_button $save_button);
  708. $pause_cont_button = $menu_frame->Button
  709. (-text => M"Pause",
  710. -fg => 'red',
  711. -width => 5,
  712. -command => \&pause_or_cont)->pack(-side => 'right');
  713. $save_button = $menu_frame->Button(-text => M"Save",
  714. -fg => 'yellow4',
  715. -command => \&save_file
  716. )->pack(-side => 'right');
  717. $balloon->attach($save_button, -msg => M"Save project data") if $balloon;
  718. use vars qw($minimized $save_geometry $up_photo $down_photo $min_button);
  719. $minimized = 0;
  720. eval {
  721. $up_photo = $top->Photo(-file => Tk::findINC("Timex/plain.up.gif"));
  722. $down_photo = $top->Photo(-file => Tk::findINC("Timex/plain.down.gif"));
  723. };
  724. warn $@ if $@;
  725. $min_button = $menu_frame->Button(-image => $up_photo,
  726. -command => \&minmaximze,
  727. -relief => 'flat',
  728. )->pack(-side => 'right');
  729. $balloon->attach($min_button, -msg => 'Minimize') if $balloon;
  730. use vars qw($busy_timer @busy_bar $busy_index $busy_string
  731. $busy_update $busy_label);
  732. @busy_bar = ('|', '/', '-', '\\');
  733. $busy_index = 0;
  734. $busy_string = " ";
  735. $busy_update = 200;
  736. $busy_label = $menu_frame->Label(-textvariable => \$busy_string,
  737. -width => 1)->pack(-side => 'right');
  738. $menu_frame->pack(-fill => 'x');
  739. state_change("menu 2 done");
  740. use vars qw($pf_cols $has_costs);
  741. $pf_cols = 5; # cannot change -columns of hlists...
  742. state_change("checkpoint 3");
  743. $project_frame = $top->Scrolled
  744. (($options->{'tree'} ? 'MyTree' : 'MyHList'),
  745. -scrollbars => "oso$sbside",
  746. -bg => $inner_bg,
  747. -fg => $inner_fg,
  748. -columns => $pf_cols,
  749. -height => 1,
  750. -drawbranch => 1,
  751. -header => 1,
  752. -selectmode => 'single',
  753. -selectbackground => 'SeaGreen3',
  754. -browsecmd => sub { },
  755. -separator => $separator,
  756. ($options->{'tree'} ? (-opencmd => sub {
  757. my($ent) = shift;
  758. $project_frame->OpenCmd($ent,@_);
  759. my $p = entry_to_project($ent);
  760. $p->closed(0) if $p;
  761. },
  762. -closecmd => sub {
  763. my($ent) = shift;
  764. $project_frame->CloseCmd($ent,@_);
  765. my $p = entry_to_project($ent);
  766. $p->closed(1) if $p;
  767. },
  768. )
  769. : ()),
  770. )->pack(-expand => 1, -fill => 'both');
  771. use vars qw($real_project_frame $is_tree);
  772. $real_project_frame = $project_frame->Subwidget("scrolled");
  773. $is_tree = ($project_frame->can('autosetmode') or
  774. ($real_project_frame and
  775. $real_project_frame->can('autosetmode')));
  776. state_change("checkpoint 4");
  777. if ($options->{'autoscroll'} !~ /^(|none)$/) {
  778. require Tk::Autoscroll;
  779. Tk::Autoscroll::Init($project_frame, -speed => $options->{'autoscroll'});
  780. }
  781. $project_frame->header('create', 0, -text => M"Projects:");
  782. use vars qw($pf_time_index);
  783. $pf_time_index = 1;
  784. $project_frame->header('create', $pf_time_index, -text => M"Session");
  785. $project_frame->header('create', $pf_time_index+1, -text => M"Today");
  786. $project_frame->header('create', $pf_time_index+2, -text => M"Total");
  787. update_costs_option(1);
  788. state_change("checkpoint 5");
  789. use vars qw($orig_selectbg);
  790. $orig_selectbg = $project_frame->cget(-selectbackground);
  791. $real_project_frame->bindtags([$real_project_frame, ref $real_project_frame,
  792. '.', 'all']);
  793. foreach my $ev (qw(Double-ButtonRelease-1
  794. Return)) {
  795. $real_project_frame->bind("<$ev>" =>
  796. sub { start() });
  797. }
  798. if ($options->{'autoscroll'} =~ /^(|none)$/) {
  799. $real_project_frame->bind("<Button-2>" =>
  800. sub { new_sub_project(get_entry(@_)) });
  801. }
  802. use vars qw($popup_entry $popup_project $popup_menu);
  803. $popup_menu = $real_project_frame->Menu(-tearoff => 0,
  804. -disabledforeground => "darkblue");
  805. $popup_menu->command(-label => M"Project:",
  806. -state => "disabled");
  807. $popup_menu->command(-label => M"New subproject",
  808. -command => sub { new_sub_project($popup_entry) })
  809. unless $s_if || $m_if;
  810. $popup_menu->command(-label => M"Delete",
  811. -command => sub { delete_project($popup_entry) })
  812. unless $s_if || $m_if;
  813. $popup_menu->command(-label => M"Continue last",
  814. -command => sub { cont_last($popup_project) });
  815. $popup_menu->command(-label => M"Attributes",
  816. -command => sub { show_attributes($popup_entry) })
  817. unless $s_if;
  818. $popup_menu->command(-label => M"Intervals",
  819. -command => sub { show_intervals($top, $popup_project) })
  820. unless $s_if;
  821. if ($real_project_frame->can("menu") &&
  822. $real_project_frame->can("PostPopupMenu") && $Tk::VERSION >= 800) {
  823. $real_project_frame->menu($popup_menu);
  824. $real_project_frame->Tk::bind('<3>' => sub {
  825. my $w = $_[0];
  826. my $e = $w->XEvent;
  827. $popup_entry = $w->GetNearest($e->y, 0);
  828. return unless defined $popup_entry;
  829. $w->anchorSet($popup_entry);
  830. $popup_project = entry_to_project($popup_entry);
  831. return unless $popup_project;
  832. $popup_menu->entryconfigure(0, -label => $popup_project->label);
  833. $w->PostPopupMenu($e->X, $e->Y);
  834. });
  835. } else {
  836. $real_project_frame->bind("<Button-3>" =>
  837. sub { show_attributes(get_entry(@_)) });
  838. }
  839. $real_project_frame->bind("<Prior>" => sub {
  840. my $w = $_[0];
  841. my $ent = $w->GetNearest(0,0);
  842. if (defined $ent) {
  843. $w->anchorSet($ent);
  844. $w->UpDown("prev");
  845. }
  846. });
  847. $real_project_frame->bind("<Next>" => sub {
  848. my $w = $_[0];
  849. my $ent = $w->GetNearest($w->height,0);
  850. if (defined $ent) {
  851. $w->anchorSet($ent);
  852. $w->UpDown("next");
  853. }
  854. });
  855. $real_project_frame->bind("<Home>" => sub {
  856. my $w = $_[0];
  857. $w->yview(moveto => 0);
  858. my $ent = $w->GetNearest(0,0);
  859. if (defined $ent) {
  860. $w->anchorSet($ent);
  861. $w->see($ent);
  862. }
  863. });
  864. $real_project_frame->bind("<End>" => sub {
  865. my $w = $_[0];
  866. $w->yview(moveto => 1);
  867. my $ent = $w->GetNearest($w->height,0);
  868. if (defined $ent) {
  869. $w->anchorSet($ent);
  870. $w->see($ent);
  871. }
  872. });
  873. state_change("checkpoint 6");
  874. if ($project_frame->can('DropSite')) {
  875. eval {
  876. $project_frame->DropSite
  877. (-dropcommand => [\&accept_drop, $project_frame],
  878. -droptypes => ($os eq 'win' ? 'Win32' : ['KDE', 'XDND', 'Sun']));
  879. };
  880. }
  881. use vars qw($status_frame);
  882. $status_frame = $top->Frame(-relief => 'ridge',
  883. -bd => 1);
  884. $status_frame->pack(-fill => 'x');
  885. $status_text = $status_frame->Label
  886. (-text => M("Current file") . ": " . ($options->{file} || ""));
  887. $status_text->pack(-side => 'left');
  888. state_change("checkpoint 7");
  889. # verzögert zeigen, da evtl. wichtige Statusmeldungen dadurch
  890. # verdeckt werden ... aber nicht, wenn in der Statuszeile editiert wird!
  891. use vars qw($status_timer);
  892. $project_frame->bind
  893. ("<Enter>" => sub {
  894. if ($status_timer) {
  895. $status_timer->cancel;
  896. undef $status_timer;
  897. }
  898. $status_timer = $project_frame->after
  899. (3000, sub { $status_text->configure(-text => project_status())
  900. unless $status_edit;
  901. })
  902. });
  903. $project_frame->bind
  904. ("<Leave>" => sub {
  905. if ($status_timer) {
  906. $status_timer->cancel;
  907. undef $status_timer;
  908. }
  909. $status_timer = $project_frame->after
  910. (3000, sub { $status_text->configure
  911. (-text => M("Current file") . ": " . ($options->{file} || ""))
  912. unless $status_edit;
  913. })
  914. });
  915. $menu_frame->UnderlineAll if $menu_frame->can('UnderlineAll');
  916. $opt->process_options if defined $opt;
  917. state_change("checkpoint 8");
  918. set_time_update();
  919. $root->modified(0);
  920. state_change("checkpoint 9");
  921. # preload file
  922. if ($options->{'file'}) {
  923. load_file(0);
  924. $last_projects = [ $root->last_projects($max_last_projects) ];
  925. create_menu_last_projects();
  926. }
  927. # merge enterprise-wide data
  928. if ($options->{'enterpriseprojects'}) {
  929. update_enterprise_projects();
  930. }
  931. $project_frame->focus;
  932. $top->repeat(5*60*1000, \&check_still_today);
  933. Tk::App::Reloader::check_loop() if defined &Tk::App::Reloader::check_loop;
  934. if ($options->{plugins}) {
  935. require Timex::Plugin;
  936. my @warnings;
  937. local $SIG{__WARN__} = sub { push @warnings, @_ };
  938. foreach my $plugin (split /\s*,\s*/, $options->{plugins}) {
  939. Timex::Plugin::load_plugin($plugin);
  940. }
  941. if (@warnings) {
  942. require Tk::DialogBox;
  943. require Tk::ROText;
  944. my $d = $top->DialogBox(-title => M"Error while loading plugins",
  945. -buttons => [M"OK"],
  946. -popover => 'cursor',
  947. );
  948. my $txt = $d->add('ROText', -width => 40, -height => 10,
  949. -relief => "flat", -borderwidth => 0,
  950. -wrap => "word",
  951. )->pack(-expand => 1, -fill => "both");
  952. $txt->insert("end", join("\n", @warnings));
  953. $d->Show;
  954. }
  955. }
  956. state_change("before MainLoop");
  957. MainLoop;
  958. ### RELOADER_START #########################################################
  959. sub enter_label {
  960. my $label = shift;
  961. my $caller = shift;
  962. my %args = @_;
  963. my $action;
  964. my $res = '';
  965. $status_edit++;
  966. $status_text->configure(-text => $label);
  967. my $Entry = "Entry";
  968. my @extra_args;
  969. my $this_history_file;
  970. my $entry;
  971. if ($args{-choices}) {
  972. $Entry = "BrowseEntry";
  973. require Tk::BrowseEntry;
  974. if ($status_browse_entry) {
  975. $entry = $status_browse_entry;
  976. } else {
  977. $entry = $status_frame->$Entry();
  978. }
  979. $entry->configure(-bg => $inner_bg,
  980. -fg => $inner_fg,
  981. -textvariable => \$res,
  982. -width => 30,
  983. -choices => $args{-choices},
  984. -state => 'readonly');
  985. } else {
  986. eval {
  987. require Tk::HistEntry;
  988. Tk::HistEntry->VERSION(0.33);
  989. $Entry = "SimpleHistEntry";
  990. # -case => 1 is ugly...
  991. @extra_args = (-match => 1, -dup => 0, -case => 0);
  992. $this_history_file = File::Spec->catfile($home, ".tktimex_hist");
  993. };
  994. $entry = $status_frame->$Entry(-bg => $inner_bg,
  995. -fg => $inner_fg,
  996. -textvariable => \$res,
  997. -width => 30,
  998. @extra_args);
  999. $entry->bindtags([ref $entry, $entry]);
  1000. if ($entry->can('historyMergeFromFile')) {
  1001. $entry->historyMergeFromFile($this_history_file);
  1002. } elsif ($entry->can('history') and ref $history{$caller} eq 'ARRAY') {
  1003. $entry->history($history{$caller});
  1004. }
  1005. if ($entry->can('history')) {
  1006. $res = ($entry->history)[-1];
  1007. $entry->selectionRange(0,"end");
  1008. }
  1009. }
  1010. $entry->pack(-side => 'left');
  1011. $entry->waitVisibility;
  1012. $entry->grab;
  1013. $entry->focus;
  1014. $entry->bind("<Return>", sub { $action = 'yes' });
  1015. $entry->bind("<Escape>", sub { $action = 'no' });
  1016. $entry->OnDestroy(sub { $status_edit-- });
  1017. $entry->waitVariable(\$action);
  1018. $entry->grabRelease;
  1019. # Muss vor $entry->destroy kommen!
  1020. if ($action eq 'yes') {
  1021. if ($entry->can('historyAdd')) {
  1022. $entry->historyAdd();
  1023. if ($entry->can('historySave')) {
  1024. $entry->historySave($this_history_file);
  1025. } else {
  1026. $history{$caller} = [ $entry->history ];
  1027. }
  1028. }
  1029. }
  1030. if ($Entry eq 'BrowseEntry') {
  1031. # This is a hack for BrowseEntry's button hack
  1032. # Problem: the toplevel binding of <ButtonRelease-1> is set
  1033. # If the Browsecentry is destroyed, this binding will go into
  1034. # empty space. Hence there is only one browse entry, which is
  1035. # only pack()ed and packForge(o)t on enter_label call.
  1036. $entry->packForget;
  1037. $status_edit--;
  1038. } else {
  1039. $entry->destroy;
  1040. }
  1041. $status_text->configure(-text => M("Current file") . ": " .
  1042. ($options->{file} || ""));
  1043. $project_frame->focus;
  1044. if ($action eq 'yes') {
  1045. $res =~ s/$separator//g; # sicherheitshalber ...
  1046. $res;
  1047. } else {
  1048. undef;
  1049. }
  1050. }
  1051. BEGIN { state_change("parsed 26%"); }
  1052. sub exists_project {
  1053. my $path = shift;
  1054. if (defined $root->find_by_pathname($path)) {
  1055. require Tk::Dialog;
  1056. $top->Dialog
  1057. (-title => M"Error",
  1058. -text => Mfmt("A project labeled %s already exists!", $path),
  1059. -popover => 'cursor',
  1060. )->Show;
  1061. return 1;
  1062. }
  1063. 0;
  1064. }
  1065. sub new_project {
  1066. my($label) = @_;
  1067. my $p;
  1068. if (!defined $label) {
  1069. $label = enter_label(M"New project name:", 'newproject');
  1070. }
  1071. if ($label && $root) {
  1072. return if exists_project($label);
  1073. $p = $root->subproject($label);
  1074. insert_project($p);
  1075. $project_frame->see(make_path($p));
  1076. }
  1077. $p;
  1078. }
  1079. sub get_templates {
  1080. return $templates_root if $templates_root;
  1081. my @templ_files;
  1082. foreach my $dir (@INC) {
  1083. push @templ_files, glob("$dir/templates/*.pjt");
  1084. push @templ_files, glob("$dir/Timex/templates/*.pjt");
  1085. }
  1086. push @templ_files, glob("$FindBin::RealBin/templates/*.pjt");
  1087. my %templ_files = map { ($_ => 1) } @templ_files;
  1088. my @templ_projects;
  1089. foreach my $f (sort keys %templ_files) {
  1090. my $tp = new Timex::Project;
  1091. $tp->load($f);
  1092. push @templ_projects, $tp;
  1093. }
  1094. $templates_root = concat Timex::Project @templ_projects;
  1095. $templates_root;
  1096. }
  1097. sub new_project_from_template {
  1098. my $template = enter_label
  1099. (M"Template:", 'template',,
  1100. -choices => [map {$_->label} get_templates()->subproject],
  1101. );
  1102. return if !$template;
  1103. my $template_p = get_templates()->find_by_pathname($template);
  1104. if (!$template_p) {
  1105. warn M"Strange: could not find project with name $template";
  1106. return;
  1107. }
  1108. my $label = enter_label(M"New project name:", 'newproject');
  1109. if ($label && $root) {
  1110. return if exists_project($label);
  1111. my $p = clone $template_p;
  1112. $p->label($label);
  1113. $p->reparent($root);
  1114. insert_project_recursive($p);
  1115. $project_frame->see(make_path($p));
  1116. }
  1117. }
  1118. sub new_sub_project {
  1119. my $path = shift;
  1120. return if !defined $path;
  1121. my $label = enter_label(M"New subproject name:", 'newproject');
  1122. if ($label) {
  1123. my $p = entry_to_project($path);
  1124. return if !$p;
  1125. my $path = $p->pathname . $separator . $label;
  1126. return if exists_project($path);
  1127. my $sub_p = $p->subproject($label);
  1128. insert_project($sub_p);
  1129. $project_frame->see(make_path($sub_p));
  1130. }
  1131. }
  1132. sub delete_project {
  1133. my $path = shift;
  1134. my $p;
  1135. if (!defined $path) {
  1136. $p = get_project_from_anchor();
  1137. } else {
  1138. return if !$project_frame->info('exists', $path);
  1139. $p = $project_frame->info('data', $path);
  1140. }
  1141. return if !$p;
  1142. return if !not_running(undef, $p);
  1143. require Tk::Dialog;
  1144. my $ans = $top->Dialog
  1145. (-title => M"Warning",
  1146. -text =>
  1147. Mfmt("Do you really want to delete the project %s" .
  1148. " and all its subprojects?", $p->pathname),
  1149. -popover => 'cursor',
  1150. -buttons => [M"Yes", M"No"],
  1151. -default_button => M"No",
  1152. )->Show;
  1153. return if $ans ne M"Yes";
  1154. $p->delete;
  1155. insert_all();
  1156. }
  1157. sub insert_project {
  1158. my($p, %args) = @_;
  1159. return if !$p;
  1160. return if $p->archived && !$options->{'archived'};
  1161. return if (defined $options->{'domain'} && $options->{'domain'} !~ /^\s*$/)
  1162. && (!defined $p->domain || $p->domain ne $options->{'domain'});
  1163. my $label = $p->label;
  1164. my $path = make_path($p);
  1165. return if !$path;
  1166. if ($project_frame->info('exists', $path)) {
  1167. warn Mfmt("Duplicate entry path %s - please check .pj1 file!",$path);
  1168. return;
  1169. }
  1170. # check existence of parent and create dummy entry, if appropriate
  1171. my $check_parent;
  1172. $check_parent = sub {
  1173. my($p, $path) = @_;
  1174. my $parent_path = get_parent_path($path);
  1175. return if $parent_path eq "";
  1176. my $parent_p = $p->parent;
  1177. return if !defined $parent_p;
  1178. if (!$project_frame->info('exists', $parent_path)) {
  1179. $check_parent->($parent_p, $parent_path);
  1180. $project_frame->add($parent_path,
  1181. -text => "(" . $parent_p->label . ")");
  1182. }
  1183. };
  1184. $check_parent->($p, $path);
  1185. $project_frame->add
  1186. ($path,
  1187. -text => $p->label,
  1188. -itemtype => $p_itemtype,
  1189. -data => $p,
  1190. # workaround for Tk804 bug: -after has to be last argument!
  1191. (defined $args{-after} ? (-after => $args{-after}) : ()),
  1192. );
  1193. if ($p_itemtype eq 'imagetext' && $p->icon) {
  1194. if (!$icons{$path}) {
  1195. eval {
  1196. $icons{$path} = $project_frame->Photo(-file => $p->icon);
  1197. }; warn $@ if $@;
  1198. }
  1199. if ($icons{$path}) {
  1200. $project_frame->itemConfigure($path, 0, -image => $icons{$path});
  1201. }
  1202. }
  1203. if ($p->archived && $is_archiv) {
  1204. $project_frame->itemConfigure($path, 0, -style => $is_archiv);
  1205. }
  1206. if ($args{-style}) {
  1207. $project_frame->itemConfigure($path, 0, -style => $args{-style});
  1208. }
  1209. if (!$p->notimes) {
  1210. my $all_time_sec = $p->sum_time(0, undef, -recursive => 1);
  1211. $project_frame->itemCreate
  1212. ($path, $pf_time_index, -itemtype => 'text',
  1213. -text => sec2time($p->sum_time($start_session_time, undef,
  1214. -recursive => 1)));
  1215. $project_frame->itemCreate
  1216. ($path, $pf_time_index+1, -itemtype => 'text',
  1217. -text => sec2time($p->sum_time($today_time, undef,
  1218. -recursive => 1)));
  1219. $project_frame->itemCreate
  1220. ($path, $pf_time_index+2, -itemtype => 'text',
  1221. -text => sec2time($all_time_sec));
  1222. if ($has_costs) {
  1223. my $hours = int($all_time_sec/3600);
  1224. $hours += ($all_time_sec%3600 > 0 ? 1 : 0);
  1225. $project_frame->itemCreate
  1226. ($path, $pf_time_index+3, -itemtype => 'text',
  1227. -text => sprintf("%.2f", $hours*hourly_rate($p)));
  1228. }
  1229. for ($pf_time_index .. $pf_time_index+($has_costs?3:2)) {
  1230. $project_frame->column('width', $_, '');
  1231. if ($east) {
  1232. $project_frame->itemConfigure($path, $_, -style => $east);
  1233. }
  1234. }
  1235. }
  1236. }
  1237. sub insert_project_recursive {
  1238. my $p = shift;
  1239. insert_project($p);
  1240. foreach my $sp ($p->subproject) {
  1241. insert_project_recursive($sp);
  1242. }
  1243. }
  1244. sub start {
  1245. my $path = shift;
  1246. my $p;
  1247. if (!$path) {
  1248. $p = get_project_from_anchor();
  1249. } else {
  1250. if ($path && ref $path and $path->can('Timex_Project_API')) {
  1251. $path = $path->pathname($separator);
  1252. }
  1253. return if !$project_frame->info('exists', $path);
  1254. $p = $project_frame->info('data', $path);
  1255. }
  1256. return if !$p;
  1257. start_project($p);
  1258. }
  1259. sub common_start_project {
  1260. my $p = shift;
  1261. stop_project();
  1262. $current_project = $p;
  1263. my $current_pathname = $p->pathname($separator);
  1264. $project_frame->selectionClear;
  1265. $project_frame->selectionSet($current_pathname);
  1266. $project_frame->anchorSet($current_pathname);
  1267. $project_frame->see($current_pathname);
  1268. gui_set_pause_or_cont(M"Pause");
  1269. $top->title($title . " (" . $p->pathname . ")");
  1270. add_last_projects($current_project);
  1271. if (defined $mb_show_curr_project_index) {
  1272. $mb_project_menu->entryconfigure($mb_show_curr_project_index,
  1273. -state => "normal");
  1274. }
  1275. if (defined $busy_timer) {
  1276. $busy_timer->cancel;
  1277. }
  1278. if ($options->{'busyind'}) {
  1279. $busy_timer = $project_frame->repeat
  1280. ($busy_update, sub {
  1281. $busy_index = ($busy_index >= $#busy_bar ? 0 : $busy_index+1);
  1282. $busy_string = $busy_bar[$busy_index];
  1283. });
  1284. }
  1285. set_timeout();
  1286. }
  1287. sub start_project {
  1288. my $p = shift;
  1289. if (!eval { notimes_check(); 1 }) {
  1290. $project_frame->selectionClear;
  1291. return;
  1292. }
  1293. common_start_project($p);
  1294. $p->start_time;
  1295. }
  1296. sub pause {
  1297. if (defined $current_project) {
  1298. stop_project();
  1299. gui_set_pause_or_cont(M"Cont");
  1300. }
  1301. }
  1302. sub pause_or_cont {
  1303. if (defined $current_project) {
  1304. stop_project();
  1305. gui_set_pause_or_cont(M"Cont");
  1306. } else {
  1307. my $p = get_project_from_anchor();
  1308. return if !$p;
  1309. start_project($p);
  1310. }
  1311. }
  1312. sub cont_last {
  1313. my $p = shift;
  1314. return unless eval { notimes_check(); 1 };
  1315. return if !not_running();
  1316. if (!$p) {
  1317. $p = get_project_from_anchor();
  1318. }
  1319. return if !$p || !@{$p->{'times'}};
  1320. my $last = $p->{'times'}[$#{$p->{'times'}}];
  1321. my $last_start = sec2time(time-$last->[0], "h", 0);
  1322. my $last_stop = sec2time(time-$last->[1], "h", 0);
  1323. require Tk::Dialog;
  1324. my $d = $top->Dialog
  1325. (-title => M"Continue last",
  1326. -text => Mfmt("Really continue time from last activity?\n" .
  1327. "%s\n" .
  1328. "started before %sh\n" .
  1329. "stopped before %sh",
  1330. $p->pathname, $last_start, $last_stop),
  1331. -default_button => M"No",
  1332. -buttons => [M"Yes", M"No"],
  1333. -popover => 'cursor',
  1334. );
  1335. return if $d->Show eq M"No";
  1336. $p->unend_time;
  1337. insert_all(); # XXX Optimierung: nur aktuelles Projekt modifizieren
  1338. common_start_project($p);
  1339. }
  1340. sub get_project_from_selection {
  1341. my $sel = $project_frame->selectionGet;
  1342. if ($sel) {
  1343. return $project_frame->info('data', $sel);
  1344. }
  1345. undef;
  1346. }
  1347. sub get_project_from_anchor {
  1348. my $sel = $project_frame->info("anchor");
  1349. if ($sel) {
  1350. return $project_frame->info('data', $sel);
  1351. }
  1352. undef;
  1353. }
  1354. sub get_project_from_anchor_or_selection {
  1355. get_project_from_anchor() || get_project_from_selection();
  1356. }
  1357. sub update_pause_cont_balloon {
  1358. my $txt = $pause_cont_button->cget(-text);
  1359. if ($txt eq M"Cont") { $txt = M"Continue" }
  1360. if ($balloon) {
  1361. my $p = get_project_from_anchor();
  1362. if ($p) {
  1363. $balloon->detach($pause_cont_button);
  1364. $balloon->attach($pause_cont_button,
  1365. -msg => $txt . " " . $p->pathname);
  1366. }
  1367. }
  1368. }
  1369. sub gui_set_pause_or_cont {
  1370. my $txt = shift;
  1371. $pause_cont_button->configure(-text => $txt,
  1372. -fg => ($txt eq M"Cont" ? 'green4' : 'red'));
  1373. update_pause_cont_balloon();
  1374. }
  1375. sub stop_project {
  1376. return unless eval { notimes_check(); 1 };
  1377. if (defined $time_after) {
  1378. $time_after->cancel;
  1379. undef $time_after;
  1380. }
  1381. if (defined $busy_timer) {
  1382. $busy_timer->cancel;
  1383. undef $busy_timer;
  1384. }
  1385. $busy_string = " ";
  1386. if (defined $current_project) {
  1387. $current_project->end_time;
  1388. act_time();
  1389. undef $current_project;
  1390. if ($options->{'autosave'}) {
  1391. save_file(); # muß nach undef $current_project kommen!!!
  1392. }
  1393. $project_frame->selectionClear;
  1394. $top->title($title);
  1395. create_menu_last_projects();
  1396. if (defined $mb_show_curr_project_index) {
  1397. $mb_project_menu->entryconfigure($mb_show_curr_project_index,
  1398. -state => "disabled");
  1399. }
  1400. }
  1401. }
  1402. sub undo {
  1403. return unless eval { notimes_check(); 1 };
  1404. if (!defined $current_project) {
  1405. require Tk::Dialog;
  1406. $top->Dialog(-title => M"Info",
  1407. -text => M"No running project.",
  1408. -default_button => M"OK",
  1409. -buttons => [M"OK"],
  1410. -popover => 'cursor',
  1411. )->Show;
  1412. return;
  1413. }
  1414. require Tk::Dialog;
  1415. my $d = $top->Dialog(-title => M"Undo",
  1416. -text => Mfmt("Really undo last start of "
  1417. . "%s ?", $current_project->pathname),
  1418. -default_button => M"No",
  1419. -buttons => [M"Yes", M"No"],
  1420. -popover => 'cursor',
  1421. );
  1422. return if $d->Show eq M"No";
  1423. my $p = $current_project;
  1424. stop_project();
  1425. $undo_register = pop(@{$p->{'times'}});
  1426. gui_set_pause_or_cont(M"Cont");
  1427. act_time($p);
  1428. }
  1429. sub tk_search_project {
  1430. my($direction) = shift;
  1431. $initial_search_direction = $direction;
  1432. $direction = +1 if !defined $direction;
  1433. my $regex = enter_label($direction < 0 ? M("Backward search:") : M("Search:"),
  1434. 'search');
  1435. search_project($regex, $direction);
  1436. }
  1437. sub search_project {
  1438. my($regex, $direction) = @_;
  1439. $direction = +1 if !defined $direction;
  1440. $direction = -$direction if $initial_search_direction < 0;
  1441. $old_search_regex = $regex;
  1442. if ($regex && $root) {
  1443. my $active;
  1444. my $symbol_dir = $direction > 0 ? "next" : "prev";
  1445. my $checkit = sub {
  1446. my $p = $project_frame->info('data', $active);
  1447. return 0 unless $p;
  1448. if ($p->pathname =~ /(?i)$regex/) {
  1449. $project_frame->see($active);
  1450. $project_frame->anchorSet($active);
  1451. return 1;
  1452. }
  1453. };
  1454. # first pass
  1455. $active = $project_frame->info("anchor");
  1456. if ($active ne "") {
  1457. $active = $project_frame->info($symbol_dir, $active);
  1458. }
  1459. while ($active ne "") {
  1460. return if $checkit->();
  1461. $active = $project_frame->info($symbol_dir, $active);
  1462. }
  1463. $status_text->configure(-text => M"Wrapped search");
  1464. # second pass from beginning or end
  1465. if ($direction > 0) {
  1466. ($active) = $project_frame->info("children");
  1467. } else {
  1468. ($active) = ($project_frame->info("children"))[-1];
  1469. while (my(@c) = $project_frame->info("children", $active)) {
  1470. $active = $c[-1];
  1471. }
  1472. }
  1473. while ($active ne "") {
  1474. return if $checkit->();
  1475. $active = $project_frame->info($symbol_dir, $active);
  1476. }
  1477. $status_text->configure(-text => M"Nothing found");
  1478. }
  1479. }
  1480. sub traverse_entries {
  1481. my $sub = shift;
  1482. my $active;
  1483. ($active) = $project_frame->info("children");
  1484. while (defined $active and $active ne "") {
  1485. $sub->($active);
  1486. $active = $project_frame->info("next", $active);
  1487. }
  1488. }
  1489. sub act_time {
  1490. my $p = shift;
  1491. if (!$p) {
  1492. $p = $current_project;
  1493. return if !$p;
  1494. }
  1495. my $project = $p;
  1496. while ($project) {
  1497. act_time_project($project);
  1498. $project = ($project->level > 1 ? $project->parent : undef);
  1499. }
  1500. set_timeout();
  1501. }
  1502. sub act_time_project {
  1503. my($p) = @_;
  1504. my $path = make_path($p);
  1505. return if !$path;
  1506. $project_frame->itemConfigure
  1507. ($path, $pf_time_index,
  1508. -text => sec2time($p->sum_time($start_session_time, undef,
  1509. -recursive => 1,
  1510. -usecache => 1)));
  1511. $project_frame->itemConfigure
  1512. ($path, $pf_time_index+1,
  1513. -text => sec2time($p->sum_time($today_time, undef,
  1514. -recursive => 1,
  1515. -usecache => 1)));
  1516. $project_frame->itemConfigure
  1517. ($path, $pf_time_index+2,
  1518. -text => sec2time($p->sum_time(0, undef,
  1519. -recursive => 1,
  1520. -usecache => 1)));
  1521. }
  1522. sub set_timeout {
  1523. if (defined $time_after) {
  1524. $time_after->cancel;
  1525. }
  1526. $time_after = $project_frame->after
  1527. ($time_update*1000, sub { act_time(); } );
  1528. }
  1529. sub working_time {
  1530. my $sum = 0;
  1531. my $week_days = 7;
  1532. my $week_work_days = 5;
  1533. my $month_days = 7*4;
  1534. my $month_work_days = 5*4;
  1535. my $last_4week_time = $today_time - 86400*$month_days;
  1536. my $last_week_time = $today_time - 86400*$week_days;
  1537. my $yesterday_time = $today_time - 86400;
  1538. my $last_4week_sum = 0;
  1539. my $last_week_sum = 0;
  1540. my $yesterday_sum = 0;
  1541. foreach ($root->subproject) {
  1542. my $project_today_time = $_->sum_time($today_time, undef,
  1543. -recursive => 1);
  1544. $sum += $project_today_time;
  1545. $yesterday_sum
  1546. += $_->sum_time($yesterday_time, undef,
  1547. -recursive => 1) - $project_today_time;
  1548. $last_week_sum
  1549. += $_->sum_time($last_week_time, undef, -recursive => 1);
  1550. $last_4week_sum
  1551. += $_->sum_time($last_4week_time, undef, -recursive => 1);
  1552. }
  1553. require Tk::DialogBox;
  1554. my $d = $top->DialogBox(-title => M"Today\'s time",
  1555. -buttons => [M"OK"],
  1556. -popover => 'cursor',
  1557. );
  1558. my $gridy = 0;
  1559. $d->add('Label',
  1560. -text => M"Today\'s working time:")->grid(-row => $gridy,
  1561. -column => 0,
  1562. -sticky => 'w');
  1563. $d->add('Label',
  1564. -text => sec2time($sum, 'h', 0))->grid(-row => $gridy,
  1565. -column => 1,
  1566. -sticky => 'w');
  1567. $gridy++;
  1568. $d->add('Label',
  1569. -text => M"Yesterday\'s working time:")->grid(-row => $gridy,
  1570. -column => 0,
  1571. -sticky => 'w');
  1572. $d->add('Label',
  1573. -text => sec2time($yesterday_sum, 'h', 0))->grid(-row => $gridy,
  1574. -column => 1,
  1575. -sticky => 'w');
  1576. $gridy++;
  1577. $d->add('Label')->grid(-row => $gridy, -column => 0);
  1578. $gridy++;
  1579. $d->add('Label',
  1580. -text => M"Last week\'s working time (8h-day):"
  1581. )->grid(-row => $gridy,
  1582. -column => 0,
  1583. -sticky => 'w');
  1584. $d->add('Label',
  1585. -text => sec2time($last_week_sum, 'd', 1))->grid(-row => $gridy,
  1586. -column => 1,
  1587. -sticky => 'w');
  1588. $gridy++;
  1589. $d->add('Label',
  1590. -text => " " . M"Average per working day:")->grid(-row => $gridy,
  1591. -column => 0,
  1592. -sticky => 'w');
  1593. $d->add('Label',
  1594. -text => sec2time($last_week_sum/$week_work_days, 'h', 0)
  1595. )->grid(-row => $gridy,
  1596. -column => 1,
  1597. -sticky => 'w');
  1598. $gridy++;
  1599. $d->add('Label')->grid(-row => $gridy, -column => 0);
  1600. $gridy++;
  1601. $d->add('Label',
  1602. -text => M"Last 4 week\'s working time (8h-day): "
  1603. )->grid(-row => $gridy,
  1604. -column => 0,
  1605. -sticky => 'w');
  1606. $d->add('Label',
  1607. -text => sec2time($last_4week_sum, 'd', 1))->grid(-row => $gridy,
  1608. -column => 1,
  1609. -sticky => 'w');
  1610. $gridy++;
  1611. $d->add('Label',
  1612. -text => " " . M"Average per working day:")->grid(-row => $gridy,
  1613. -column => 0,
  1614. -sticky => 'w');
  1615. $d->add('Label',
  1616. -text => sec2time($last_4week_sum/$month_work_days, 'h', 0)
  1617. )->grid(-row => $gridy,
  1618. -column => 1,
  1619. -sticky => 'w');
  1620. $d->Show;
  1621. }
  1622. sub show_one_day {
  1623. return unless eval { notimes_check(); 1 };
  1624. my($inner_bg_opt, $inner_fg_opt) = ('-bg', '-fg');
  1625. my %date_args;
  1626. my $has_date;
  1627. eval $date_require;
  1628. my $one_day_only = 1;
  1629. my $has_date_entry;
  1630. if (!$has_date) {
  1631. eval {
  1632. require Tk::DateEntry;
  1633. };
  1634. $has_date_entry = !$@;
  1635. if (!$has_date_entry) {
  1636. require Time::Local;
  1637. }
  1638. }
  1639. my $f = $top->Toplevel(-title => M"Show daily details");
  1640. $f->{WindowType} = "Daily details"; # no M
  1641. my $btn;
  1642. my @p; # project array for one day
  1643. my($dw, $dw_to);
  1644. my $no_interval_cb;
  1645. my $adjust_chain;
  1646. if ($has_date) {
  1647. my $df = $f->Frame->pack;
  1648. # from:
  1649. $dw = $df->Date
  1650. ($inner_bg_opt => $inner_bg,
  1651. $inner_fg_opt => $inner_fg,
  1652. %date_args,
  1653. -fields => 'date',
  1654. -value => 'now',
  1655. -datefmt => "%12A, %2d.%2m.%4y",
  1656. -choices => [qw(today yesterday),
  1657. [M"one week before" => sub {time()-86400*7}],
  1658. [M"four weeks before" => sub { time()-86400*7*4}],
  1659. ],
  1660. -command => sub {
  1661. # XXX if chain button activated, adjust dw_to widget
  1662. if ($options->{'oneday-immediately'}) {
  1663. $btn->invoke;
  1664. }
  1665. }
  1666. )->grid(-row => 0, -column => 0, -sticky => "e");
  1667. # to:
  1668. $dw_to = $df->Date
  1669. ($inner_bg_opt => $inner_bg,
  1670. $inner_fg_opt => $inner_fg,
  1671. %date_args,
  1672. -fields => 'date',
  1673. -value => 'now',
  1674. -datefmt => "%12A, %2d.%2m.%4y",
  1675. -choices => [qw(today yesterday),
  1676. [M"one week before" => sub {time()-86400*7}],
  1677. [M"four weeks before" => sub { time()-86400*7*4}],
  1678. ],
  1679. -command => sub {
  1680. if ($options->{'oneday-immediately'}) {
  1681. $btn->invoke;
  1682. }
  1683. }
  1684. )->grid(-row => 1, -column => 0, -sticky => "e");
  1685. my $c;
  1686. my $setup_chain = sub {
  1687. $dw_to->update; # XXX wollte ich eigentlich vermeiden
  1688. my $h = $dw_to->y - $dw->y + $dw_to->height;
  1689. my $h_step = $h*5/40;
  1690. $c = $df->Canvas
  1691. (-width => 20,
  1692. -height => $h,
  1693. -takefocus => 0,
  1694. -highlightthickness => 0,
  1695. )->grid(-row => 0, -column => 1, -rowspan => 2);
  1696. $c->createLine(5,$h_step*2, 10,$h_step*2,
  1697. 15,$h_step*3, 15,$h_step*5,
  1698. 10,$h_step*6, 5,$h_step*6,
  1699. -width => $h_step, -smooth => 1,
  1700. -tags => "chain");
  1701. my $orig_bg = $c->cget(-bg);
  1702. $c->Tk::bind('<Enter>' => sub {
  1703. $c->itemconfigure("chain", -fill => 'grey50');
  1704. });
  1705. $c->Tk::bind('<Leave>' => sub {
  1706. $c->itemconfigure("chain", -fill => "black");
  1707. });
  1708. $adjust_chain = sub {
  1709. if ($one_day_only) {
  1710. $c->delete("broken");
  1711. } else {
  1712. $c->createRectangle(0,16/40*$h,20,23/40*$h,-fill=>$orig_bg,
  1713. -outline=>undef,
  1714. -tags=>"broken");
  1715. }
  1716. };
  1717. $c->Tk::bind('<1>' => sub {
  1718. $no_interval_cb->toggle;
  1719. $adjust_chain->();
  1720. });
  1721. };
  1722. $df->afterIdle($setup_chain);
  1723. } else {
  1724. if ($has_date_entry) {
  1725. $dw = $f->DateEntry
  1726. (-dateformat => 2,
  1727. -background => $inner_bg,
  1728. -foreground => $inner_fg,
  1729. -daynames => [qw/So Mo Di Mi Do Fr Sa/],
  1730. -weekstart => 1,
  1731. )->pack;
  1732. } else {
  1733. $dw = $f->Entry(-bg => $inner_bg,
  1734. -fg => $inner_fg,
  1735. )->pack;
  1736. $dw->bind('<Return>' => sub {
  1737. if ($options->{'oneday-immediately'}) {
  1738. $btn->invoke;
  1739. }
  1740. });
  1741. }
  1742. my(@l) = localtime;
  1743. $dw->insert(0, sprintf("%04d/%02d/%02d", $l[5]+1900, $l[4]+1, $l[3]));
  1744. }
  1745. my $ff = $f->Frame->pack;
  1746. $btn = $ff->Button(-text => M"Show")->pack(-side => 'left');
  1747. $f->{InvokeButton} = $btn;
  1748. my $clb = $ff->Button(-text => M"Close",
  1749. -command => sub { $f->destroy },
  1750. )->pack(-side => 'left');
  1751. $f->bind('<Escape>' => sub { $clb->invoke });
  1752. $ff->Checkbutton(-text => M"immediately",
  1753. -variable => \$options->{'oneday-immediately'},
  1754. -command => sub {
  1755. $btn->invoke;
  1756. },
  1757. ($has_date_entry ? (-state => 'disabled') : ()),
  1758. )->pack(-side => 'left');
  1759. $no_interval_cb =
  1760. $ff->Checkbutton(-text => M"no interval",
  1761. -variable => \$one_day_only,
  1762. -command => sub {
  1763. if ($options->{'oneday-immediately'}) {
  1764. $btn->invoke;
  1765. }
  1766. $adjust_chain->() if $adjust_chain;
  1767. },
  1768. )->pack(-side => 'left');
  1769. my $act_from_date; # current from date
  1770. my $lb = $f->Scrolled('HList',
  1771. -bg => $inner_bg,
  1772. -fg => $inner_fg,
  1773. -columns => 2,
  1774. -width => 40,
  1775. -header => 1,
  1776. -scrollbars => "oso$sbside",
  1777. -selectmode => 'extended',
  1778. -exportselection => 1,
  1779. -command => sub {
  1780. show_intervals($f, $p[$_[0]],
  1781. -day => $act_from_date)
  1782. },
  1783. )->pack(-expand => 1,
  1784. -fill => 'both');
  1785. $btn->configure(-command => sub {
  1786. my $s_from;
  1787. if ($has_date) {
  1788. $s_from = $dw->get("%s");
  1789. $s_from = Tk::Date::_begin_of_day($s_from);
  1790. } else {
  1791. my $s = $dw->get;
  1792. my($y,$m,$d) = split(/\D/, $s);
  1793. return if !($d >= 1 && $d <= 31 &&
  1794. $m >= 1 && $m <= 12 &&
  1795. defined $y);
  1796. $y -= 1900 if $y > 1900;
  1797. $s_from = Time::Local::timelocal(0, 0, 0,
  1798. $d, $m-1, $y);
  1799. }
  1800. $act_from_date = $s_from;
  1801. my $s_to = $s_from + 86399;
  1802. if ($has_date && !$one_day_only) {
  1803. $s_to = $dw_to->get("%s");
  1804. $s_to = Tk::Date::_begin_of_day($s_to)+86399;
  1805. }
  1806. if ($has_date) {
  1807. $dw_to->configure(-value => $s_to);
  1808. }
  1809. @p = $root->projects_by_interval($s_from, $s_to);
  1810. $lb->delete('all');
  1811. my $i = 0;
  1812. my $sum = 0;
  1813. foreach (@p) {
  1814. $lb->add($i, -text => $_->pathname);
  1815. my $diff = $_->sum_time($s_from, $s_to);
  1816. $sum += $diff;
  1817. $lb->itemCreate($i, 1, -text =>
  1818. sec2time($diff, undef, undef));
  1819. $i++;
  1820. }
  1821. $lb->header('create', 0, -text => '*** sum ***');
  1822. $lb->header('create', 1, -text =>
  1823. sec2time($sum, undef, undef));
  1824. });
  1825. if ($has_date) {
  1826. my $di = $lb->Button
  1827. (-text => M"Daily intervals",
  1828. -command => sub {
  1829. my $begin_date = $dw->get("%s");
  1830. $begin_date = Tk::Date::_begin_of_day($begin_date);
  1831. daily_intervals($begin_date, $begin_date+86400-1);
  1832. },
  1833. -padx => 0, -pady => 0);
  1834. $di->place(-rely => 1, '-y' => -$di->reqheight,
  1835. -relx => 1, '-x' => -$di->reqwidth); # XXX place!
  1836. }
  1837. if ($options->{'oneday-immediately'}) { $btn->invoke }
  1838. $f->Popup(-popover => 'cursor');
  1839. }
  1840. sub daily_intervals {
  1841. my($begin_date, $end_date) = @_;
  1842. require POSIX;
  1843. my $t = $top->Toplevel;
  1844. $t->title(POSIX::strftime("%Y-%m-%d", localtime $begin_date));
  1845. my $c;
  1846. my $lb;
  1847. my $highlight_sub = sub {
  1848. my $entry = shift;
  1849. $c->delete("hi");
  1850. foreach my $it ($c->find("withtag", "entry_$entry")) {
  1851. my $new_it = canvas_copy_item($c, $it);
  1852. $c->itemconfigure($new_it, -fill => "green", -tags => "hi");
  1853. }
  1854. };
  1855. my @utmp_lines;
  1856. if ($utmp) {
  1857. $utmp->update_if_necessary(300);
  1858. @utmp_lines = $utmp->restrict(User => $username,
  1859. From => $begin_date,
  1860. To => $end_date);
  1861. }
  1862. $lb = $t->Scrolled
  1863. ('HList', -scrollbars => "oso$sbside",
  1864. -columns => 3,
  1865. -width => 60,
  1866. -selectmode => "browse",
  1867. -browsecmd => $highlight_sub,
  1868. )->pack(-expand => 1, -fill => 'both',
  1869. -side => "left");
  1870. my $rad = 50;
  1871. $c = $t->Canvas(-width => $rad*2,
  1872. -height => $rad*2*2+5,
  1873. -takefocus => 0,
  1874. -highlightthickness => 0,
  1875. )->pack(-fill => "both", -side => "left");
  1876. my @clock = ([0,0,$rad*2,$rad*2],
  1877. [0,$rad*2+4, $rad*2, $rad*2]
  1878. );
  1879. $c->createOval(@{$clock[0]},
  1880. -outline => $inner_fg, -fill => $inner_bg, -width => 3);
  1881. _draw_hour_ticks($c, @{$clock[0]});
  1882. $c->createOval($clock[1]->[0], $clock[1]->[1],
  1883. $clock[1]->[0]+$clock[1]->[2],
  1884. $clock[1]->[1]+$clock[1]->[3],
  1885. -outline => $inner_fg, -fill => $inner_bg, -width => 3);
  1886. _draw_hour_ticks($c, @{$clock[1]});
  1887. $c->bind("entry", "<1>" => sub {
  1888. my $c = shift;
  1889. foreach ($c->gettags("current")) {
  1890. if (/^entry_(\d+)/) {
  1891. my $e = $1;
  1892. $lb->see($e);
  1893. $lb->anchorClear;
  1894. $lb->selectionClear;
  1895. $lb->anchorSet($e);
  1896. $highlight_sub->($e);
  1897. return;
  1898. }
  1899. }
  1900. });
  1901. my $str_time = sub { POSIX::strftime("%H:%M:%S", localtime $_[0]) };
  1902. my @res_times = $root->restricted_times($begin_date, $end_date);
  1903. my $i = 0;
  1904. my $fill_color = "red";
  1905. foreach (@res_times) {
  1906. my $name = $_->[0]->pathname;
  1907. if (length($name) > 40) {
  1908. $name = "... " . substr($name, -36); #length($name)-36
  1909. }
  1910. $lb->add($i, -text => $name,
  1911. -itemtype => $p_itemtype,
  1912. ($hl_entry{$fill_color}
  1913. ? (-style => $hl_entry{$fill_color}) : ()));
  1914. my $begin_time = $str_time->($_->[1]);
  1915. my $end_time = $str_time->($_->[2]);
  1916. $lb->itemCreate($i, 1, -text => $begin_time,
  1917. -itemtype => $p_itemtype,
  1918. ($hl_entry{$fill_color}
  1919. ? (-style => $hl_entry{$fill_color}) : ()));
  1920. $lb->itemCreate($i, 2, -text => $end_time,
  1921. -itemtype => $p_itemtype,
  1922. ($hl_entry{$fill_color}
  1923. ? (-style => $hl_entry{$fill_color}) : ()));
  1924. my($begin_clock,
  1925. $begin_x,
  1926. $begin_y,
  1927. $begin_angle) = _get_tic_pos($c, $clock[0], $clock[1], $begin_time);
  1928. my($end_clock,
  1929. $end_x,
  1930. $end_y,
  1931. $end_angle) = _get_tic_pos($c, $clock[0], $clock[1], $end_time);
  1932. if ($begin_clock == $end_clock) {
  1933. $c->createArc($clock[$begin_clock]->[0], $clock[$begin_clock]->[1],
  1934. $clock[$begin_clock]->[0]+$clock[$begin_clock]->[2],
  1935. $clock[$begin_clock]->[1]+$clock[$begin_clock]->[3],
  1936. -start => $begin_angle,
  1937. -extent => $end_angle-$begin_angle,
  1938. -fill => $fill_color,
  1939. -tags => ["entry_$i", "entry"],
  1940. );
  1941. } else {
  1942. $c->createArc($clock[0]->[0], $clock[0]->[1],
  1943. $clock[0]->[0]+$clock[0]->[2],
  1944. $clock[0]->[1]+$clock[0]->[3],
  1945. -start => $begin_angle,
  1946. -extent => 90-$begin_angle,
  1947. -fill => $fill_color,
  1948. -tags => ["entry_$i", "entry"],
  1949. );
  1950. $c->createArc($clock[1]->[0], $clock[1]->[1],
  1951. $clock[1]->[0]+$clock[1]->[2],
  1952. $clock[1]->[1]+$clock[1]->[3],
  1953. -start => 90,
  1954. -extent => $end_angle-(90+360),
  1955. -fill => $fill_color,
  1956. -tags => ["entry_$i", "entry"],
  1957. );
  1958. }
  1959. $fill_color = ($fill_color eq 'red' ? 'blue' : 'red');
  1960. $i++;
  1961. }
  1962. my @utmp_canvas_args = (-fill => 'yellow',
  1963. -stipple => 'gray50',
  1964. -tags => "uptime",
  1965. -outline => undef,
  1966. );
  1967. foreach my $utmp_line (@utmp_lines) {
  1968. my $begin_time = $str_time->($utmp_line->{Begin});
  1969. my $end_time = $str_time->($utmp_line->{End});
  1970. my($begin_clock,
  1971. $begin_x,
  1972. $begin_y,
  1973. $begin_angle) = _get_tic_pos($c, $clock[0], $clock[1], $begin_time);
  1974. my($end_clock,
  1975. $end_x,
  1976. $end_y,
  1977. $end_angle) = _get_tic_pos($c, $clock[0], $clock[1], $end_time);
  1978. if ($begin_clock == $end_clock) {
  1979. $c->createArc($clock[$begin_clock]->[0], $clock[$begin_clock]->[1],
  1980. $clock[$begin_clock]->[0]+$clock[$begin_clock]->[2],
  1981. $clock[$begin_clock]->[1]+$clock[$begin_clock]->[3],
  1982. -start => $begin_angle,
  1983. -extent => $end_angle-$begin_angle,
  1984. @utmp_canvas_args,
  1985. );
  1986. } else {
  1987. $c->createArc($clock[0]->[0], $clock[0]->[1],
  1988. $clock[0]->[0]+$clock[0]->[2],
  1989. $clock[0]->[1]+$clock[0]->[3],
  1990. -start => $begin_angle,
  1991. -extent => 90-$begin_angle,
  1992. @utmp_canvas_args,
  1993. );
  1994. $c->createArc($clock[1]->[0], $clock[1]->[1],
  1995. $clock[1]->[0]+$clock[1]->[2],
  1996. $clock[1]->[1]+$clock[1]->[3],
  1997. -start => 90,
  1998. -extent => $end_angle-(90+360),
  1999. @utmp_canvas_args,
  2000. );
  2001. }
  2002. }
  2003. $c->raise('uptime');
  2004. $c->raise('entry');
  2005. $c->raise('tics');
  2006. }
  2007. sub _draw_hour_ticks {
  2008. my $c = shift;
  2009. my($x, $y, $width, $height) = @_;
  2010. for my $h (0..11) {
  2011. $c->createLine
  2012. (
  2013. $x + $width/2-sin(deg2rad((12-$h)*30))*$width/2,
  2014. $y + $height/2-cos(deg2rad((12-$h)*30))*$height/2,
  2015. $x + $width/2-sin(deg2rad((12-$h)*30))*($width/2-8),
  2016. $y + $height/2-cos(deg2rad((12-$h)*30))*($height/2-8),
  2017. -fill => "black",
  2018. -width => 3,
  2019. -tags => "tics",
  2020. );
  2021. }
  2022. }
  2023. sub _get_tic_pos {
  2024. my($c, $clock1_def, $clock2_def, $time) = @_;
  2025. my $clock = 0;
  2026. if ($time =~ /^(\d{1,2}):(\d{2}):(\d{2})/) {
  2027. my $hour = $1;
  2028. my $min = $2;
  2029. if ($hour >= 12) {
  2030. $clock = 1;
  2031. $hour-=12;
  2032. }
  2033. $hour += $min/60;
  2034. my $clock_def = ($clock == 0 ? $clock1_def : $clock2_def);
  2035. my $angle = (12-$hour)*30;
  2036. ($clock,
  2037. $clock_def->[0] + $clock_def->[2]/2-sin(deg2rad($angle))
  2038. * $clock_def->[2]/2,
  2039. $clock_def->[1] + $clock_def->[3]/2-cos(deg2rad($angle))
  2040. * $clock_def->[3]/2,
  2041. $angle+90,
  2042. );
  2043. } else {
  2044. ();
  2045. }
  2046. }
  2047. # REPO BEGIN
  2048. # REPO NAME copy_item /home/e/eserte/src/repository
  2049. # REPO MD5 839315861d37edfcdfd81060ab32d9e4
  2050. sub canvas_copy_item {
  2051. my($c, $i) = @_;
  2052. my $type = $c->type($i);
  2053. my @coords = $c->coords($i);
  2054. my @old_config = $c->itemconfigure($i);
  2055. my @new_config;
  2056. foreach my $conf (@old_config) {
  2057. push @new_config, $conf->[0], $conf->[4];
  2058. }
  2059. $c->create($type, @coords, @new_config);
  2060. }
  2061. # REPO END
  2062. # REPO BEGIN
  2063. # REPO NAME standalone_message_box /home/e/eserte/src/repository
  2064. # REPO MD5 c4592f93ed4afa4f6a93d9ff38d2e905
  2065. sub standalone_message_box {
  2066. my %args = @_;
  2067. require Tk;
  2068. my $mw_created;
  2069. my(@mw) = Tk::MainWindow::Existing();
  2070. if (!@mw) {
  2071. push @mw, MainWindow->new();
  2072. $mw[0]->withdraw;
  2073. $mw_created++;
  2074. }
  2075. $args{-icon} = 'error' unless exists $args{-icon};
  2076. $args{-title} = M"Error" unless exists $args{-error};
  2077. $args{-type} = "OK" unless exists $args{-type};
  2078. my $answer = $mw[0]->messageBox(%args);
  2079. if ($mw_created) {
  2080. $mw[0]->destroy;
  2081. }
  2082. $answer;
  2083. }
  2084. # REPO END
  2085. sub _multiproject {
  2086. my(@files) = @_;
  2087. require Timex::MultiProject;
  2088. my $mp1 = Timex::MultiProject->new;
  2089. $mp1->set(-masterproject => $root,
  2090. -files => \@files);
  2091. $mp1;
  2092. }
  2093. sub load_files {
  2094. my $mp1 = _multiproject(@_);
  2095. $mp1->load or return 0;
  2096. $mp1->save; # try to save updated project files
  2097. $mp1->master_project; # return master project
  2098. }
  2099. sub save_files {
  2100. _multiproject(@_)->save;
  2101. }
  2102. sub load_file {
  2103. my $interactive = shift;
  2104. my $file_to_load = shift;
  2105. my $offline_file;
  2106. if ($root->modified || defined $current_project) {
  2107. require Tk::Dialog;
  2108. my $dialog = $top->Dialog(-title => M"Load",
  2109. -text => M"Load project data (overwrite current data)?",
  2110. -default_button => M"No",
  2111. -buttons => [M"Yes", M"No"],
  2112. -popover => 'cursor',
  2113. );
  2114. return if $dialog->Show eq M"No";
  2115. }
  2116. stop_project();
  2117. if (!defined $file_to_load) {
  2118. $file_to_load = $options->{'file'};
  2119. $offline_file = $options->{'offlinefile'};
  2120. }
  2121. if ($interactive) {
  2122. my($file, $path) = fileparse($options->{'file'});
  2123. if ($path =~ m|^\.|) {
  2124. require Cwd;
  2125. $path = Cwd::abs_path($path);
  2126. }
  2127. $file_to_load = get_filename($top,
  2128. -Title => M"Enter project file",
  2129. -File => $file,
  2130. -Path => $path,
  2131. -FPat => '*.pj1',
  2132. -filetypes => [qw/pj1 xml all/],
  2133. -Create => 0);
  2134. return if !$file_to_load;
  2135. $offline_file = '';
  2136. }
  2137. if ($is_opened and $lock_is_strict) {
  2138. unlock_file_temp();
  2139. }
  2140. $root->delete_all;
  2141. if (!lock_file($file_to_load)) {
  2142. $options->{'file'} = '';
  2143. return;
  2144. }
  2145. my $sos_file = sos_filename($file_to_load);
  2146. if (-f $sos_file) {
  2147. my $mtime = (stat($sos_file))[9];
  2148. require Tk::Dialog;
  2149. $top->Dialog
  2150. (-title => M"Warning",
  2151. -text => Mfmt
  2152. ("There is a sos file <%s>\n".
  2153. "from %s\n".
  2154. "You should check whether this file contains valueable information.\n".
  2155. "Otherwise delete the file to avoid this warning.",
  2156. $sos_file, scalar(localtime($mtime))),
  2157. -popover => 'cursor',
  2158. -default_button => M"OK",
  2159. -buttons => [M"OK"])->Show;
  2160. }
  2161. my $load_root = new Timex::Project;
  2162. unlock_file_temp() if $lock_is_strict;
  2163. my $ok = 1;
  2164. if ($offline_file ne '') {
  2165. $load_root = load_files($file_to_load, $offline_file);
  2166. if (!$load_root) {
  2167. $status_text->configure(-text => $@);
  2168. $ok = 0;
  2169. }
  2170. } else {
  2171. if (!$load_root->load($file_to_load)) {
  2172. $status_text->configure(-text => $@);
  2173. $ok = 0;
  2174. }
  2175. }
  2176. return unless $ok;
  2177. # no check if there is also an offline file
  2178. if ($offline_file eq '') {
  2179. $file_writeable = -w $file_to_load;
  2180. if (!$file_writeable) {
  2181. require Tk::Dialog;
  2182. $top->Dialog
  2183. (-title => M"Warning",
  2184. -text => Mfmt("The file %s is not writeable!", $file_to_load),
  2185. -popover => 'cursor',
  2186. )->Show;
  2187. }
  2188. lock_file($file_to_load) if $lock_is_strict;
  2189. }
  2190. $root = $load_root;
  2191. $options->{'file'} = $file_to_load;
  2192. $status_text->configure(-text => Mfmt("Loaded %s", $options->{file}));
  2193. insert_all();
  2194. set_autosave();
  2195. $root->modified(0);
  2196. if ($mod_watch) {
  2197. $mod_watch->Unwatch;
  2198. $mod_watch = Tie::Watch->new(-variable => \$root->{'modified'},
  2199. -store => $mod_sub,
  2200. );
  2201. $mod_sub->();
  2202. } else {
  2203. $save_check->configure(-variable => \$root->{'modified'});
  2204. }
  2205. my $last_project = $root->last_project;
  2206. if ($last_project) {
  2207. my $last_project_path = make_path($last_project);
  2208. if ($project_frame->info('exists', $last_project_path)) {
  2209. $project_frame->anchorSet($last_project_path);
  2210. $project_frame->see($last_project_path);
  2211. gui_set_pause_or_cont(M"Cont");
  2212. }
  2213. }
  2214. }
  2215. BEGIN { state_change("parsed 54%"); }
  2216. sub load_file_noninteractive {
  2217. my $file = shift;
  2218. load_file(0, $file);
  2219. }
  2220. sub lock_file {
  2221. #warn "file=$_[0] lock:$can_lock";
  2222. if (!$can_lock || !$options->{'lock'}) {
  2223. return 1;
  2224. }
  2225. my $file = shift;
  2226. if (!-e $file) {
  2227. return 1;
  2228. }
  2229. if (!$is_opened) {
  2230. #warn "not opened";
  2231. eval q{
  2232. use Fcntl qw(:flock);
  2233. flock CURRFILE, LOCK_UN;
  2234. };
  2235. warn $@ if $@;
  2236. close CURRFILE;
  2237. $is_opened = 0;
  2238. }
  2239. my $lock_ok = 0;
  2240. if (open(CURRFILE, $file)) {
  2241. $is_opened = 1;
  2242. #warn "opend";
  2243. eval q{
  2244. use Fcntl qw(:flock);
  2245. #warn "try flock";
  2246. if (!flock CURRFILE, LOCK_EX|LOCK_NB) {
  2247. use Tk::Dialog;
  2248. $top->Dialog
  2249. (-title => M"File locked",
  2250. -text => Mfmt
  2251. ("<%s> is already locked.\n\n" .
  2252. "Please check that there is no other tktimex process " .
  2253. "using this file and try again.\n", $file),
  2254. -default_button => M"OK",
  2255. -buttons => [M"OK"],
  2256. -popover => 'cursor',
  2257. )->Show;
  2258. $lock_ok = 0;
  2259. } else {
  2260. $lock_ok = 1;
  2261. }
  2262. };
  2263. warn $@ if $@;
  2264. }
  2265. #warn "lockok=$lock_ok";
  2266. $lock_ok;
  2267. }
  2268. sub unlock_file_temp {
  2269. #warn "file=? unlock: can_lock=$can_lock";
  2270. return if (!$can_lock || !$options->{'lock'});
  2271. eval q{
  2272. use Fcntl qw(:flock);
  2273. #warn "try lock";
  2274. flock CURRFILE, LOCK_UN;
  2275. };
  2276. warn $@ if $@;
  2277. close CURRFILE;
  2278. #warn "cloce";
  2279. $is_opened = 0;
  2280. }
  2281. sub update_project {
  2282. my $p = shift;
  2283. # get top parent of this project (one under root)
  2284. my $top_parent = $p->top_parent;
  2285. # get hlist position (i.e. previous element
  2286. my $top_parent_entry = make_path($top_parent);
  2287. if (!$top_parent_entry) {
  2288. warn Mfmt("Should not happen: Can't find entry for %s",
  2289. $top_parent->pathname);
  2290. return;
  2291. }
  2292. my @root_children = $project_frame->info("children");
  2293. my $prev_entry;
  2294. SEARCH:
  2295. {
  2296. for my $i (0 .. $#root_children) {
  2297. if ($top_parent_entry eq $root_children[$i]) {
  2298. $prev_entry = $root_children[$i-1] if $i > 0;
  2299. last SEARCH;
  2300. }
  2301. }
  2302. warn Mfmt("Can't find %s in children list (@root_children) of HList",$top_parent_entry);
  2303. return;
  2304. }
  2305. $project_frame->delete("entry", $top_parent_entry);
  2306. insert_old_project($top_parent, -after => $prev_entry);
  2307. }
  2308. sub insert_all {
  2309. my(%args) = @_;
  2310. my %new_p;
  2311. if ($args{-newprojects}) {
  2312. %new_p = map { ($_->pathname, 1) } @{ $args{-newprojects} };
  2313. }
  2314. my %changed_p;
  2315. if ($args{-changedprojects}) {
  2316. %changed_p = map { ($_->pathname, 1) } @{ $args{-changedprojects} };
  2317. }
  2318. $top->Busy($Tk::VERSION >= 800.014 ? (-recurse => 1) : ());
  2319. $project_frame->delete('all');
  2320. my $p;
  2321. foreach $p ($root->sorted_subprojects($options->{'sort'})) {
  2322. insert_old_project($p,
  2323. -newprojects => \%new_p,
  2324. -changedprojects => \%changed_p);
  2325. }
  2326. if (defined $current_project) {
  2327. $project_frame->anchorSet(make_path($current_project));
  2328. }
  2329. @all_domains = $root->get_all_domains;
  2330. if ($is_tree) {
  2331. # custom setmode implementation to use the recorded closed information
  2332. # in the project file
  2333. my $setmode;
  2334. $setmode = sub {
  2335. my ($ent,$mode) = @_;
  2336. unless (defined $mode) {
  2337. $mode = 'none';
  2338. my @args;
  2339. push(@args,$ent) if defined $ent;
  2340. my @children = $project_frame->infoChildren( @args );
  2341. if ( @children ) {
  2342. my $p = entry_to_project($ent);
  2343. $mode = $p && $p->closed ? 'open' : 'close';
  2344. foreach my $c (@children) {
  2345. if ($mode eq 'open') {
  2346. $project_frame->hide(-entry => $c);
  2347. } else {
  2348. $mode = 'open' if $project_frame->infoHidden( $c );
  2349. }
  2350. $setmode->( $c );
  2351. }
  2352. }
  2353. }
  2354. if (defined $ent) {
  2355. if ( $mode eq 'open' ) {
  2356. $project_frame->_indicator_image( $ent, 'plus' );
  2357. } elsif ( $mode eq 'close' ) {
  2358. $project_frame->_indicator_image( $ent, 'minus' );
  2359. } elsif( $mode eq 'none' ) {
  2360. $project_frame->_indicator_image( $ent, undef );
  2361. }
  2362. }
  2363. };
  2364. $setmode->();
  2365. }
  2366. $top->Unbusy($Tk::VERSION >= 800.014 ? (-recurse => 1) : ());
  2367. }
  2368. *update_all = \&insert_all;
  2369. sub insert_old_project {
  2370. my($p, %args) = @_;
  2371. my $prev_entry = delete $args{-after};
  2372. my $style;
  2373. my $set_style = sub {
  2374. my $p = shift;
  2375. my $style;
  2376. if ($args{-newprojects} && $args{-newprojects}->{$p->pathname}) {
  2377. $style = $new_in_merge;
  2378. }
  2379. if ($args{-changedprojects} && $args{-changedprojects}->{$p->pathname}) {
  2380. $style = $changed_in_merge;
  2381. }
  2382. $style;
  2383. };
  2384. insert_project($p, -after => $prev_entry, -style => $set_style->($p));
  2385. if (!$options->{'onlytop'}) {
  2386. foreach ($p->sorted_subprojects($options->{'sort'})) {
  2387. insert_old_project($_, %args);
  2388. }
  2389. }
  2390. }
  2391. sub update_costs_option {
  2392. my $interactive = shift;
  2393. $has_costs = ($options->{'hourlyrate'} > 0) ? 1 : 0;
  2394. if ($interactive) {
  2395. insert_all();
  2396. my $costs_col = $pf_time_index+3;
  2397. if ($interactive && !$has_costs) {
  2398. if ($project_frame->header('exist', $costs_col)) {
  2399. $project_frame->header('delete', $costs_col);
  2400. }
  2401. } else {
  2402. $project_frame->header
  2403. ('create', $costs_col,
  2404. -text => Mfmt("Cost (%s %s)",
  2405. $options->{'hourlyrate'},
  2406. $options->{'currency'}));
  2407. }
  2408. }
  2409. }
  2410. sub dump_data {
  2411. print STDERR $root->dump_data();
  2412. if (!open(OUT, ">/tmp/timex.data")) {
  2413. $status_text->configure(-text => M("Can't write to timex.data").": $!");
  2414. return;
  2415. }
  2416. print OUT $root->dump_data();
  2417. close OUT;
  2418. }
  2419. sub old_save_file {
  2420. eval { require Data::Dumper };
  2421. if ($@) {
  2422. $status_text->configure(-text => $@);
  2423. return;
  2424. }
  2425. return if (!$options->{'file'});
  2426. my $datafile = "$options->{'file'}.data";
  2427. if ($^O =~ /(mswin|dos)/i) {
  2428. $datafile =~ s/\.pj1//; # strip first extension
  2429. }
  2430. if (!open(OUT, ">$datafile")) {
  2431. $status_text->configure
  2432. (-text => Mfmt("Can't write to <%s>: %s", $datafile, $!));
  2433. return;
  2434. }
  2435. my $dd = new Data::Dumper [$root], ['root'];
  2436. # Indent(0) for buggy Data::Dumper on some ActivePerl versions
  2437. eval { $dd->Purity(1)->Indent(0) }; # eval for versions before 2.081
  2438. my $dump;
  2439. eval { $dump = $dd->Dumpxs };
  2440. if ($@) {
  2441. $dump = $dd->Dump;
  2442. }
  2443. print OUT $dump, "\n";
  2444. close OUT;
  2445. }
  2446. sub save_file {
  2447. my($autosave) = @_;
  2448. if (!$options->{'file'}) {
  2449. if (!$autosave) {
  2450. return save_as_file(@_);
  2451. } else {
  2452. return;
  2453. }
  2454. }
  2455. if (defined $current_project) {
  2456. $current_project->end_time;
  2457. }
  2458. my @collect_warnings;
  2459. my $dir_check_done;
  2460. my $rename_op = sub {
  2461. my $inx = shift;
  2462. my $from = (defined $inx ? "$options->{'file'}.$inx" : $options->{'file'});
  2463. my $to = "$options->{'file'}." . (defined $inx ? $inx+1 : 1);
  2464. if (-e $from) {
  2465. if (!rename $from, $to) {
  2466. push @collect_warnings, Mfmt("Could not rename %s to %s: %s", $from, $to, $!);
  2467. if (!$dir_check_done) {
  2468. $dir_check_done++;
  2469. my $dir = dirname($to);
  2470. if (!-w $dir) {
  2471. push @collect_warnings, Mfmt("The directory %s is not writable for you", $dir);
  2472. }
  2473. }
  2474. }
  2475. }
  2476. };
  2477. if (!$autosave) {
  2478. foreach (reverse(0 .. 8)) {
  2479. $rename_op->($_);
  2480. }
  2481. }
  2482. $rename_op->(undef);
  2483. if (@collect_warnings) {
  2484. if ($top && Tk::Exists($top) && $top->can('messageBox')) {
  2485. my $yesno =
  2486. $top->messageBox(-message => M("Problem while renaming backup files. Please contact your system administrator or check permissions.\nThe detailed error message is:\n") .
  2487. join("\n", @collect_warnings) . "\n\n" .
  2488. M("Do you want to continue the save operation?"),
  2489. -icon => 'error',
  2490. -title => M"Save problem",
  2491. -type => 'YesNo',
  2492. );
  2493. if ($yesno !~ /yes/i) {
  2494. return 0;
  2495. }
  2496. } else {
  2497. warn join("\n", @collect_warnings);
  2498. }
  2499. }
  2500. unlock_file_temp() if $lock_is_strict;
  2501. my $offline_file = $options->{'offlinefile'};
  2502. my $ret;
  2503. if (defined $offline_file && $offline_file ne "") {
  2504. $ret = save_files($options->{'file'}, $offline_file);
  2505. } else {
  2506. $ret = $root->save("$options->{'file'}");
  2507. }
  2508. if (!$ret) {
  2509. $status_text->configure(-text => $@);
  2510. } else {
  2511. $status_text->configure(-text => Mfmt("Saved <%s>",$options->{'file'}));
  2512. }
  2513. old_save_file() if $options->{'securesave'};
  2514. lock_file("$options->{'file'}") if $lock_is_strict;
  2515. if (defined $current_project) {
  2516. $current_project->unend_time;
  2517. }
  2518. if (!$autosave) {
  2519. $root->modified(0);
  2520. }
  2521. lock_file($options->{'file'});
  2522. set_autosave();
  2523. }
  2524. sub save_as_file {
  2525. my $autosave = shift;
  2526. my($file, $path) = get_file_path();
  2527. $file = get_filename($top,
  2528. -Title => M"Enter project file",
  2529. -File => $file,
  2530. -Path => $path,
  2531. -FPat => '*.pj1',
  2532. -filetypes => [qw/pj1 all/],
  2533. -Create => 1);
  2534. return unless $file;
  2535. $file = adjust_filename($file);
  2536. $options->{'file'} = $file;
  2537. save_file($autosave);
  2538. }
  2539. sub sos_filename {
  2540. my $file = shift;
  2541. dirname($file) . "/#" . basename($file) . "#";
  2542. }
  2543. sub save_sos {
  2544. return if !$root || !$root->modified;
  2545. my $file;
  2546. if (!$options->{'file'}) {
  2547. $file = sos_filename(File::Spec->catfile($home, "tktimex.pj1"));
  2548. } else {
  2549. $file = sos_filename($options->{'file'});
  2550. }
  2551. if (defined $current_project) {
  2552. $current_project->end_time;
  2553. }
  2554. warn Mfmt("Saving sos file %s...\n", $file);
  2555. $root->save($file);
  2556. if (defined $current_project) {
  2557. $current_project->unend_time;
  2558. }
  2559. eval {
  2560. require Mail::Send;
  2561. my $msg = Mail::Send->new;
  2562. $msg->to($username);
  2563. $msg->subject(M"tktimex: sos file");
  2564. my $fh = $msg->open;
  2565. print $fh Mfmt(<<EOF, $file);
  2566. A copy of your tktimex data is saved in %s.
  2567. Please check whether the data is complete, then copy this file
  2568. as your tktimex data file with:
  2569. EOF
  2570. print $fh <<EOF;
  2571. @{[
  2572. $os eq 'win' ? "copy" : "cp"
  2573. ]} $file $options->{'file'}
  2574. EOF
  2575. $fh->close;
  2576. };
  2577. warn $@ if $@;
  2578. }
  2579. sub _overwrite_warning {
  2580. my $file = shift;
  2581. if (-e $file) {
  2582. require Tk::Dialog;
  2583. die if ($top->Dialog
  2584. (-title => M"Warning",
  2585. -text => Mfmt
  2586. ("Really overwrite %s with skeleton data?\n".
  2587. "All time information will be lost in %s!",$file,$file),
  2588. -popover => 'cursor',
  2589. -default_button => M"No",
  2590. -buttons => [M"Yes", M"No"])->Show ne M"Yes");
  2591. }
  2592. }
  2593. sub save_skeleton {
  2594. my($file, $path) = get_file_path();
  2595. $file = get_filename($top,
  2596. -Title => M"Enter skeleton project file",
  2597. -Path => $path,
  2598. -FPat => '*.pj1',
  2599. -filetypes => [qw/pjt pj1 all/],
  2600. -Create => 1);
  2601. return unless $file;
  2602. $file = adjust_filename($file);
  2603. eval {
  2604. _overwrite_warning($file);
  2605. };
  2606. return if ($@);
  2607. $root->save($file, -skeleton => 1);
  2608. }
  2609. sub save_subproject {
  2610. my $p = get_project_from_anchor();
  2611. return if !$p;
  2612. my($file, $path) = get_file_path();
  2613. $file = get_filename($top,
  2614. -Title => M"Enter project file",
  2615. -Path => $path,
  2616. -FPat => '*.pj1',
  2617. -filetypes => [qw/pj1 all/],
  2618. -Create => 1);
  2619. return unless $file;
  2620. $file = adjust_filename($file);
  2621. eval {
  2622. _overwrite_warning($file);
  2623. };
  2624. return if ($@);
  2625. $p->save($file);
  2626. }
  2627. sub save_xml {
  2628. my($file, $path) = get_file_path();
  2629. $file = get_filename($top,
  2630. -Title => M"Enter XML project file",
  2631. -File => $file,
  2632. -Path => $path,
  2633. -FPat => '*.xml',
  2634. -filetypes => [qw/xml all/],
  2635. -Create => 1);
  2636. return unless $file;
  2637. $file = adjust_filename($file, ".xml");
  2638. require Timex::Project::XML;
  2639. my $clone = clone Timex::Project::XML $root;
  2640. $clone->save($file);
  2641. }
  2642. sub merge_file {
  2643. my $path;
  2644. $path = $options->{'mergedir'};
  2645. if (!defined $path || !-d $path) {
  2646. (undef, $path) = fileparse($options->{'file'});
  2647. }
  2648. my $file = get_filename($top,
  2649. -Title => M"Enter project file for merge",
  2650. -Path => $path,
  2651. -FPat => '*.pj1',
  2652. -filetypes => [qw/pj1 pjt all/],
  2653. -Create => 0);
  2654. return unless $file;
  2655. $options->{'mergedir'} = dirname($file);
  2656. merge_file_noninteractive($file);
  2657. }
  2658. sub merge_file_noninteractive {
  2659. my $file = shift;
  2660. my %args = @_;
  2661. ###XXXX del:
  2662. # my %load_args;
  2663. # $load_args{-skeleton} = delete $load_args{-skeleton};
  2664. my $new_project = new Timex::Project;
  2665. if (!$new_project->load($file, %args)) {
  2666. $status_text->configure(-text => $@);
  2667. return;
  2668. }
  2669. my($diff, $new_p_ref, $changed_p_ref) = $root->merge($new_project);
  2670. insert_all(-newprojects => $new_p_ref,
  2671. -changedprojects => $changed_p_ref) if $diff;
  2672. $status_text->configure
  2673. (-text => Mfmt("Merge completed with %s %s", $diff,
  2674. ($diff == 1 ? M("difference") : M("differences"))),
  2675. );
  2676. }
  2677. sub update_enterprise_projects {
  2678. if (!$options->{'enterpriseprojects'}) {
  2679. require Tk::Dialog;
  2680. $top->Dialog
  2681. (-title => M"Error",
  2682. -text => M
  2683. ("There is no enterprise projects file defined.\n" .
  2684. "Please go to the enterprise tab in the option editor.\n"),
  2685. -popover => 'cursor')->Show;
  2686. return;
  2687. }
  2688. if (!-r $options->{'enterpriseprojects'}) {
  2689. require Tk::Dialog;
  2690. $top->Dialog
  2691. (-title => M"Error",
  2692. -text => Mfmt("File %s is not readable or does not exist.\n",
  2693. $options->{'enterpriseprojects'}),
  2694. -popover => 'cursor')->Show;
  2695. return;
  2696. }
  2697. merge_file_noninteractive($options->{'enterpriseprojects'},
  2698. -skeleton => 1);
  2699. }
  2700. sub get_filename {
  2701. my($top, %args) = @_;
  2702. my %change_opt;
  2703. my $defaultextension;
  2704. if ($args{'-FPat'}) {
  2705. if ($Tk::VERSION <= 800.011) {
  2706. ($defaultextension = $args{'-FPat'}) =~ s/^\*\.//;
  2707. } else {
  2708. ($defaultextension = $args{'-FPat'}) =~ s/^\*//;
  2709. }
  2710. }
  2711. my $types = [];
  2712. if ($args{-filetypes}) {
  2713. foreach my $type (@{ $args{-filetypes} }) {
  2714. if ($type eq 'pj1') {
  2715. push @$types, [M"Timex files", '.pj1'];
  2716. } elsif ($type eq 'all') {
  2717. push @$types, [M"All files", '*'];
  2718. } elsif ($type eq 'xml') {
  2719. push @$types, [M"Timex XML files", '.xml'];
  2720. } elsif ($type eq 'pjt') {
  2721. push @$types, [M"Timex Template files", '.pjt'];
  2722. } elsif ($type eq 'gif') {
  2723. push @$types, [M"GIF images", '.gif'];
  2724. } elsif ($type eq 'xpm') {
  2725. push @$types, [M"X11 pixmaps", '.xpm'];
  2726. } elsif ($type eq 'xbm') {
  2727. push @$types, [M"X11 bitmaps", '.xbm'];
  2728. } elsif ($type eq 'ppm') {
  2729. push @$types, [M"PPM images", '.ppm'];
  2730. } elsif ($type eq 'bmp') {
  2731. push @$types, [M"BMP images", '.bmp'];
  2732. } elsif ($type eq 'images') {
  2733. push @$types, [M"Images", ['.ppm','.gif','.xpm','.xbm','.bmp']];
  2734. }
  2735. }
  2736. }
  2737. if ($args{-Create} && $top->can('getSaveFile')) {
  2738. my $file = $top->getSaveFile
  2739. (-initialdir => $args{-Path},
  2740. -initialfile => $args{'File'},
  2741. -defaultextension => $defaultextension,
  2742. -title => $args{-Title},
  2743. ($Tk::VERSION >= 800.012 ? (-filetypes => $types) : ()),
  2744. );
  2745. return $file;
  2746. } elsif (!$args{-Create} && $top->can('getOpenFile')) {
  2747. my $file = $top->getOpenFile
  2748. (-initialdir => $args{-Path},
  2749. -defaultextension => $defaultextension,
  2750. -title => $args{-Title},
  2751. ($Tk::VERSION >= 800.012 ? (-filetypes => $types) : ()),
  2752. );
  2753. return $file;
  2754. }
  2755. my $filedialog = 'FileDialog';
  2756. if ($os eq 'win') {
  2757. $@ = "XXX Tk::FileDialog does not work with win32";
  2758. } else {
  2759. eval { require Tk::FileDialog };
  2760. }
  2761. if ($@) {
  2762. warn "Harmless warning:\n$@\n";
  2763. require Tk::FileSelect;
  2764. $filedialog = 'FileSelect';
  2765. %change_opt = (-FPat => '-filter',
  2766. -Path => '-directory',
  2767. -File => undef,
  2768. -Create => undef,
  2769. -Title => undef,
  2770. );
  2771. }
  2772. foreach (keys %args) {
  2773. if (exists $change_opt{$_}) {
  2774. if (defined $change_opt{$_}) {
  2775. $args{$change_opt{$_}} = delete $args{$_};
  2776. } else {
  2777. delete $args{$_};
  2778. }
  2779. }
  2780. }
  2781. my $fd = $top->$filedialog(%args);
  2782. $fd->Show(-popover => 'cursor');
  2783. }
  2784. sub get_file_path {
  2785. my($file, $path);
  2786. if ($options->{'file'}) {
  2787. ($file, $path) = fileparse($options->{'file'});
  2788. } else {
  2789. $file = "";
  2790. $path = $home || "/";
  2791. }
  2792. ($file, $path);
  2793. }
  2794. sub set_autosave {
  2795. if ($options->{'autosave'}) {
  2796. if (defined $autosave_after) {
  2797. $autosave_after->cancel;
  2798. }
  2799. $autosave_after = $top->after($options->{'update'}*1000,
  2800. sub { save_file(1) });
  2801. }
  2802. }
  2803. sub toggle_autosave {
  2804. set_autosave();
  2805. }
  2806. sub set_dateformat {
  2807. insert_all();
  2808. if (set_time_update()) {
  2809. set_timeout();
  2810. }
  2811. foreach my $w ($top->Descendants("Toplevel")) {
  2812. # no M:
  2813. if ($w->{WindowType} and $w->{WindowType} eq "Daily details" and
  2814. $w->{InvokeButton}) {
  2815. $w->{InvokeButton}->invoke;
  2816. }
  2817. }
  2818. }
  2819. sub not_running {
  2820. my($var, $p) = @_;
  2821. my $project_is_running;
  2822. if (defined $p and defined $current_project and $p eq $current_project) {
  2823. $project_is_running = 1;
  2824. }
  2825. if (!defined $p and defined $current_project) {
  2826. $project_is_running = 1;
  2827. }
  2828. if ($project_is_running) {
  2829. require Tk::Dialog;
  2830. $top->Dialog(-title => 'Warning',
  2831. -text =>
  2832. "Can't perform this action while project running",
  2833. -popover => 'cursor',
  2834. )->Show;
  2835. if (defined $var) {
  2836. # alte Einstellung wiederherstellen
  2837. $$var = ($$var ? 0 : 1);
  2838. }
  2839. return undef;
  2840. } else {
  2841. return 1;
  2842. }
  2843. }
  2844. sub toggle_show_archived {
  2845. if (not_running(\$options->{'archived'})) {
  2846. insert_all();
  2847. }
  2848. }
  2849. sub toggle_show_only_top {
  2850. not_running(\$options->{'onlytop'}) && insert_all();
  2851. }
  2852. sub toggle_time_arbeitstag {
  2853. insert_all();
  2854. }
  2855. sub _fix_be {
  2856. $_[0]->Subwidget("entry")->Subwidget("entry")->configure
  2857. (-bg => $inner_bg, -fg => $inner_fg);
  2858. };
  2859. sub _all_labels {
  2860. map { $_->[1] }
  2861. sort { $a->[0] cmp $b->[0] }
  2862. map { [lc($_), $_] }
  2863. $root->all_pathnames;
  2864. }
  2865. sub _all_projects_browseentry {
  2866. my($parent, %args) = @_;
  2867. my $exclude_root = delete $args{-excluderoot};
  2868. require Tk::BrowseEntry;
  2869. my $browse = $parent->BrowseEntry(%args);
  2870. _fix_be($browse);
  2871. # Verwendung des Schwartzian Transform wegen Problemen mit lc.
  2872. # Es ist vielleicht auch marginal schneller.
  2873. foreach (_all_labels) {
  2874. if (!defined $_ || $_ eq '') {
  2875. next if $exclude_root;
  2876. $_ = '(Root)'
  2877. }
  2878. $browse->insert("end", $_);
  2879. }
  2880. $browse;
  2881. }
  2882. sub show_attributes {
  2883. my($path, $readonly) = @_;
  2884. my $readonly_some;
  2885. if (defined $current_project) { $readonly_some = 1 }
  2886. if (!defined $path) {
  2887. $path = get_sel_entry();
  2888. return if !defined $path;
  2889. }
  2890. my $project = $project_frame->info('data', $path);
  2891. return if !defined $project;
  2892. my $attribute_top = $top->Toplevel(-title => M"Attributes");
  2893. my $f = $attribute_top->Frame->pack(-fill => 'both', -expand => 1);
  2894. my $row = 0;
  2895. my $dframe = sub {
  2896. my $ff = $f->Frame->grid(-padx => 1,
  2897. -row => $row, -column => 1, -sticky => 'w');
  2898. $ff->Label->pack(-side => "left");
  2899. $ff;
  2900. };
  2901. # Name/Id ##########
  2902. $f->Label(-text => M('Name').': ')->grid(-row => $row, -column => 0,
  2903. -sticky => 'w');
  2904. my $label = $project->label;
  2905. my $ff1 = $dframe->();
  2906. my $name_entry = $ff1->Entry(-bg => $inner_bg,
  2907. -fg => $inner_fg,
  2908. -textvariable => \$label
  2909. )->pack(-side => "left");
  2910. $name_entry->focus;
  2911. if ($readonly || $readonly_some) {
  2912. $name_entry->configure(-state => 'disabled');
  2913. }
  2914. $f->Label(-text => M("Id").": " . $project->id)->grid(-row => $row,
  2915. -column => 2,
  2916. -sticky => "e");
  2917. # Old parent ##########
  2918. if ($project->parent) {
  2919. $row++;
  2920. $f->Label(-text => M('Parent').':')->grid(-row => $row,
  2921. -column => 0,
  2922. -sticky => 'w');
  2923. $ff1 = $dframe->();
  2924. $ff1->Label(-text => ($project->parent eq $root ?
  2925. '('.M("Root").')' : $project->parent->label)
  2926. )->pack(-side => "left");
  2927. }
  2928. # New parent ##########
  2929. $row++;
  2930. my $new_parent;
  2931. $f->Label(-text => M("New Parent"))->grid(-row => $row,
  2932. -column => 0,
  2933. -sticky => 'w');
  2934. my $browse = _all_projects_browseentry
  2935. ($f,
  2936. -variable => \$new_parent,
  2937. $readonly || $readonly_some ? (-state => 'disabled') : (),
  2938. );
  2939. $browse->grid(-row => $row, -column => 1,
  2940. -columnspan => 1, -sticky => 'w');
  2941. # Rate ##########
  2942. my $rate = my $old_rate = $project->{'rate'};
  2943. $row++;
  2944. $f->Label(-text => M('Rate') .
  2945. (defined $options->{'currency'}
  2946. ? " (" . $options->{'currency'} . ")"
  2947. : "")
  2948. )->grid(-row => $row, -column => 0, -sticky => 'w');
  2949. $ff1 = $dframe->();
  2950. my $rate_entry = $ff1->Entry
  2951. (-textvariable => \$rate,
  2952. -bg => $inner_bg,
  2953. -fg => $inner_fg,
  2954. )->pack(-side => "left");
  2955. if ($readonly) { $rate_entry->configure(-state => 'disabled') }
  2956. # Domain ##########
  2957. my $domain = my $old_domain = $project->{'domain'};
  2958. if (!defined $domain) {
  2959. $domain = $project->domain;
  2960. if (defined $domain) {
  2961. $domain = "($domain)";
  2962. }
  2963. }
  2964. $row++;
  2965. $f->Label(-text => M"Domain"
  2966. )->grid(-row => $row, -column => 0, -sticky => 'w');
  2967. my $domain_entry = $f->BrowseEntry
  2968. (-textvariable => \$domain,
  2969. -choices => ["", @all_domains],
  2970. )->grid(-row => $row, -column => 1, -columnspan => 1, -sticky => 'w');
  2971. if ($readonly) { $domain_entry->configure(-state => 'disabled') }
  2972. _fix_be($domain_entry);
  2973. # Archived ##########
  2974. my $archived = $project->{'archived'};
  2975. $row++;
  2976. my $arch_check = $f->Checkbutton
  2977. (-text => M"Archived",
  2978. -variable => \$archived
  2979. )->grid(-row => $row, -column => 0, -sticky => 'w');
  2980. if ($readonly) { $arch_check->configure(-state => 'disabled') }
  2981. my $PathEntry = "Entry";
  2982. if (eval 'require Tk::PathEntry; 1') {
  2983. $PathEntry = 'PathEntry';
  2984. }
  2985. # RCS/CVS file ##########
  2986. my $rcsfile = $project->rcsfile;
  2987. $row++;
  2988. $f->Label(-text => M("RCS/CVS file").":"
  2989. )->grid(-row => $row, -column => 0, -sticky => 'w');
  2990. $ff1 = $dframe->();
  2991. my $rcs_entry = $ff1->$PathEntry(-bg => $inner_bg,
  2992. -fg => $inner_fg,
  2993. -textvariable => \$rcsfile
  2994. )->pack(-side => "left");
  2995. my $browse_entry = $f->Button
  2996. (-text => M("Browse")."...",
  2997. -command => sub {
  2998. my($file, $path) = fileparse($rcsfile) if $rcsfile;
  2999. my $newfile = get_filename
  3000. ($attribute_top,
  3001. -Title => M"RCS/CVS file",
  3002. ($rcsfile ? (-File => $file,
  3003. -Path => $path) : ()),
  3004. -Create => 0,
  3005. -filetypes => [qw/pj1 pjt xml all/],
  3006. );
  3007. if ($newfile) {
  3008. $rcsfile = $newfile;
  3009. }
  3010. })->grid(-row => $row, -column => 2, -sticky => 'w');
  3011. if ($readonly) {
  3012. $rcs_entry->configure(-state => 'disabled');
  3013. $browse_entry->configure(-state => 'disabled');
  3014. }
  3015. # Icon ##########
  3016. my $iconfile = $project->icon;
  3017. $row++;
  3018. $f->Label(-text => M("Icon file").":"
  3019. )->grid(-row => $row, -column => 0, -sticky => 'w');
  3020. $ff1 = $dframe->();
  3021. my $icon_entry = $ff1->$PathEntry(-bg => $inner_bg,
  3022. -fg => $inner_fg,
  3023. -textvariable => \$iconfile
  3024. )->pack(-side => "left");
  3025. my $icon_browse_entry = $f->Button
  3026. (-text => M("Browse")."...",
  3027. -command => sub {
  3028. my($file, $path) = fileparse($iconfile) if $iconfile;
  3029. my $newfile = get_filename
  3030. ($attribute_top,
  3031. -Title => M"Icon file",
  3032. ($iconfile ? (-File => $file,
  3033. -Path => $path) : ()),
  3034. -Create => 0,
  3035. -filetypes => [qw/images xpm gif xbm ppm bmp/],
  3036. );
  3037. if ($newfile) {
  3038. $iconfile = $newfile;
  3039. }
  3040. })->grid(-row => $row, -column => 2, -sticky => 'w');
  3041. if ($readonly) {
  3042. $icon_entry->configure(-state => 'disabled');
  3043. $icon_browse_entry->configure(-state => 'disabled');
  3044. }
  3045. if ($PathEntry eq 'PathEntry') {
  3046. foreach my $w ($rcs_entry, $icon_entry) {
  3047. foreach my $k (qw/Return Escape/) {
  3048. $w->bind("<$k>" => [$w, 'Finish']);
  3049. }
  3050. }
  3051. }
  3052. # Job number ##########
  3053. my $jobnumber = $project->jobnumber;
  3054. $row++;
  3055. $f->Label(-text => M("Job number").":")->grid(-row => $row,
  3056. -column => 0,
  3057. -sticky => "w");
  3058. my $jne = $f->Entry(-textvariable => \$jobnumber,
  3059. -bg => $inner_bg,
  3060. -fg => $inner_fg,
  3061. )->grid(-row => $row,
  3062. -column => 1,
  3063. -sticky => "we");
  3064. my $jobnumbers_browse;
  3065. if (defined &main::browse_jobnumbers) {
  3066. $jobnumbers_browse = $f->Button
  3067. (-text => M("Browse")."...",
  3068. -command => sub {
  3069. my $new_jobnumber = main::browse_jobnumbers($attribute_top);
  3070. if (defined $new_jobnumber) {
  3071. $jobnumber = $new_jobnumber;
  3072. }
  3073. })->grid(-row => $row, -column => 2, -sticky => 'w');
  3074. }
  3075. if ($readonly) {
  3076. $jne->configure(-state => 'disabled');
  3077. $jobnumbers_browse->configure(-state => 'disabled') if $jobnumbers_browse;
  3078. }
  3079. # Show intervals/Note ##########
  3080. $row++;
  3081. my $ff = $f->Frame->grid(-row => $row, -column => 0,
  3082. -columnspan => 3, -sticky => "w");
  3083. $ff->Button(-text => M"Show intervals",
  3084. -command => sub {
  3085. show_intervals($f,
  3086. $project,
  3087. -readonly => $readonly);
  3088. })->pack(-side => "left");
  3089. my $note_label = M"Note";
  3090. if ($project->has_note) {
  3091. $note_label .= " *";
  3092. }
  3093. $ff->Button(-text => $note_label,
  3094. -command => sub {
  3095. show_note($top);
  3096. })->pack(-side => "left");
  3097. # OK/Cancel ##########
  3098. my $command_frame = $attribute_top->Frame->pack(-fill => 'x',
  3099. -expand => 1);
  3100. my $ok = $command_frame->Button
  3101. (-command => sub {
  3102. my $insert_all;
  3103. if ($label && $label ne $project->label) {
  3104. $project->label($label);
  3105. $insert_all++;
  3106. }
  3107. if (defined $new_parent && grep($_ eq $new_parent, _all_labels)) {
  3108. my $new_parent_p;
  3109. if ($new_parent eq '(Root)') {
  3110. $new_parent_p = $root;
  3111. } else {
  3112. $new_parent_p = $root->find_by_pathname($new_parent);
  3113. }
  3114. if ($new_parent_p) {
  3115. if ($project->reparent($new_parent_p)) {
  3116. $insert_all++;
  3117. } else {
  3118. require Tk::Dialog;
  3119. #XXX übersetzen
  3120. $attribute_top->Dialog
  3121. (-title => M"Warning",
  3122. -text =>
  3123. "Can't reparent " . $project->label . " to " .
  3124. $new_parent_p->label,
  3125. -popover => 'cursor',
  3126. )->Show;
  3127. }
  3128. }
  3129. }
  3130. {
  3131. local $^W = undef;
  3132. $insert_all++ if ($archived && !$project->{'archived'});
  3133. }
  3134. $project->archived($archived);
  3135. $project->rcsfile($rcsfile);
  3136. {
  3137. local $^W = undef;
  3138. $insert_all++ if ($iconfile ne $project->{'iconfile'});
  3139. }
  3140. $project->icon($iconfile);
  3141. $project->jobnumber($jobnumber);
  3142. {
  3143. local $^W = undef;
  3144. $insert_all++ if $rate ne $old_rate;
  3145. }
  3146. $project->rate($rate);
  3147. {
  3148. local $^W = undef;
  3149. $insert_all++ if $domain ne $old_domain;
  3150. }
  3151. $project->domain($domain);
  3152. $attribute_top->destroy();
  3153. insert_all() if $insert_all;
  3154. }
  3155. );
  3156. set_text_or_image($ok, "yes.gif", M"OK");
  3157. $ok->pack(-side => 'left');
  3158. if ($readonly) { $ok->focus }
  3159. my $cancel = $command_frame->Button
  3160. (-command => sub { $attribute_top->destroy() }
  3161. );
  3162. $attribute_top->bind('<Escape>' => sub { $cancel->invoke });
  3163. set_text_or_image($cancel, "no.gif", M"Cancel");
  3164. $cancel->pack(-side => 'left');
  3165. $attribute_top->Popup(-popover => 'cursor');
  3166. }
  3167. BEGIN { state_change("parsed 74%"); }
  3168. sub show_intervals {
  3169. my($top, $project, %args) = @_;
  3170. return unless eval { notimes_check(); 1 };
  3171. my $readonly = $args{-readonly};
  3172. my $show_seconds = $args{-show_seconds};
  3173. my $group = $args{-group} || '';
  3174. my $geometry = $args{-geometry};
  3175. my $modified = $args{-modified};
  3176. my $day = $args{-day};
  3177. my $subproj = $args{-subproj};
  3178. my $w = $args{-toplevel};
  3179. if (!Tk::Exists($w)) {
  3180. undef $w;
  3181. } else {
  3182. $_->destroy for ($w->children);
  3183. }
  3184. if (!defined $project) {
  3185. $project = entry_to_project(get_sel_entry());
  3186. return if !defined $project;
  3187. }
  3188. if ($group eq 'weekly') {
  3189. eval {
  3190. require Date::Calc;
  3191. };
  3192. if ($@) {
  3193. warn "$@. " . M"Reverting to daily";
  3194. $group = "daily";
  3195. }
  3196. }
  3197. #$top->Busy($Tk::VERSION >= 800.014 ? (-recurse => 1) : ());
  3198. my @rev;
  3199. if ($project->rcsfile) {
  3200. $top->Busy($Tk::VERSION >= 800.014 ? (-recurse => 1) : ());
  3201. eval {
  3202. my $rcs = get_rcs_from_cache($project);
  3203. if ($rcs) {
  3204. foreach my $rev ($rcs->revisions) {
  3205. push(@rev, [$rev->revision,
  3206. $rev->unixtime,
  3207. scalar $rcs->symbolic_name($rev)]);
  3208. }
  3209. } else {
  3210. die "Can't create rcs/cvs object";
  3211. }
  3212. };
  3213. warn $@ if $@;
  3214. $top->Unbusy($Tk::VERSION >= 800.014 ? (-recurse => 1) : ());
  3215. }
  3216. my($lb, $real_lb, $times);
  3217. my $update = sub {
  3218. $args{-toplevel} = $w;
  3219. $args{-show_seconds} = $show_seconds;
  3220. $args{-group} = $group;
  3221. $args{-modified} = $modified;
  3222. $args{-subproj} = $subproj;
  3223. show_intervals($top, $project,
  3224. %args);
  3225. };
  3226. my $log_viewer = sub {
  3227. my $i = shift;
  3228. if ($project->rcsfile) {
  3229. eval {
  3230. my $rcs = get_rcs_from_cache($project);
  3231. if ($rcs) {
  3232. my $log_entries = $rcs->get_log_entries
  3233. (@{$times->[$i]}[0..1]);
  3234. my $t = $w->Toplevel
  3235. (-title => "Log entries for "
  3236. . $project->pathname . " "
  3237. . join(" - ", map { scalar localtime $_ }
  3238. @{$times->[$i]}[0..1]));
  3239. my $ok = $t->Button(-text => 'OK',
  3240. -command => sub {
  3241. $t->destroy;
  3242. })->pack(-side => "bottom");
  3243. $t->bind('<Escape>' => sub { $ok->invoke });
  3244. require Tk::ROText;
  3245. my $ro = $t->Scrolled
  3246. ("ROText", -scrollbars => "so$sbside",
  3247. -wrap => "none",
  3248. )->pack(-fill => "both", -expand => 1);
  3249. $ro->insert("end", $log_entries);
  3250. $ok->focus;
  3251. }
  3252. }
  3253. }
  3254. };
  3255. my $epoch2readable_date = sub {
  3256. my @l = localtime $_[0];
  3257. sprintf "%04d-%02d-%02d-%02d:%02d:%02d",
  3258. $l[5]+1900, $l[4]+1, $l[3], $l[2], $l[1], $l[0];
  3259. };
  3260. my $readable_date2epoch = sub {
  3261. if ($_[0] =~ /^\s*(\d{4})-(\d{1,2})-(\d{1,2})-(\d{1,2}):(\d{2}):(\d{2})\s*$/) {
  3262. require Time::Local;
  3263. Time::Local::timelocal($6, $5, $4, $3, $2-1, $1-1900);
  3264. } else {
  3265. undef;
  3266. }
  3267. };
  3268. my $interval_editor = sub {
  3269. my $i = shift;
  3270. my %args = @_;
  3271. return if $readonly;
  3272. my($inner_bg_opt, $inner_fg_opt) = ('-bg', '-fg');
  3273. my %date_args;
  3274. my $has_date;
  3275. eval $date_require;
  3276. my $t = $w->Toplevel(-title => "Edit line $i for "
  3277. . $project->pathname);
  3278. my $row = 0;
  3279. my($from, $to, $annotation) = @{$times->[$i]};
  3280. my $date_choices = ['now'];
  3281. if ($utmp) {
  3282. $utmp->update_if_necessary(300);
  3283. my @utmp_lines = $utmp->restrict(User => $username,
  3284. From => $today_time,
  3285. To => time,
  3286. );
  3287. if (@utmp_lines) {
  3288. push @$date_choices,
  3289. ['Today login' => $utmp_lines[-1]->{Begin}],
  3290. ['Today logout' => $utmp_lines[0]->{End}];
  3291. }
  3292. @utmp_lines = $utmp->restrict(User => $username,
  3293. From => $today_time-86400,
  3294. To => $today_time-1);
  3295. if (@utmp_lines) {
  3296. push @$date_choices,
  3297. ['Yesterday login' => $utmp_lines[-1]->{Begin}],
  3298. ['Yesterday logout' => $utmp_lines[0]->{End}];
  3299. }
  3300. };
  3301. $t->Label(-text => M("From").":")->grid(-row => $row, -column => 0);
  3302. if (!$has_date) {
  3303. $from = $epoch2readable_date->($from);
  3304. $to = $epoch2readable_date->($to);
  3305. }
  3306. my $from_date = ($has_date
  3307. ? $t->Date($inner_bg_opt => $inner_bg,
  3308. $inner_fg_opt => $inner_fg,
  3309. %date_args,
  3310. -variable => \$from,
  3311. -choices => $date_choices,
  3312. )
  3313. : $t->Entry(-bg => $inner_bg,
  3314. -fg => $inner_fg,
  3315. -textvariable => \$from)
  3316. );
  3317. $from_date->grid(-row => $row++, -column => 1);
  3318. $t->Label(-text => M("To").":")->grid(-row => $row, -column => 0);
  3319. my $to_date = ($has_date
  3320. ? $t->Date($inner_bg_opt => $inner_bg,
  3321. $inner_fg_opt => $inner_fg,
  3322. %date_args,
  3323. -variable => \$to,
  3324. -choices => $date_choices,
  3325. )
  3326. : $t->Entry(-bg => $inner_bg,
  3327. -fg => $inner_fg,
  3328. -textvariable => \$to)
  3329. );
  3330. $to_date->grid(-row => $row++, -column => 1);
  3331. $t->Label(-text => M("Annotation").":")->grid(-row => $row, -column => 0);
  3332. $t->Entry(-textvariable => \$annotation)->grid(-row => $row++, -column => 1, -sticky => "ew");
  3333. my $f = $t->Frame->grid(-row => $row++, -column => 0, -columnspan => 2);
  3334. my $okb = $f->Button
  3335. (-text => 'OK',
  3336. -command => sub {
  3337. my($from_e, $to_e, $annotation_e);
  3338. if (!$has_date) {
  3339. $from_e = $readable_date2epoch->($from);
  3340. $to_e = $readable_date2epoch->($to);
  3341. if (!defined $from_e || !defined $to_e) {
  3342. die "Can't recognize $from/$to";
  3343. }
  3344. } else {
  3345. ($from_e, $to_e) = ($from, $to);
  3346. }
  3347. if (defined $annotation && $annotation !~ /^\s*$/) {
  3348. $annotation_e = $annotation;
  3349. }
  3350. $project->set_times($i, $from_e, $to_e, $annotation);
  3351. $update->($t);
  3352. },
  3353. )->pack(-side => 'left');
  3354. my $cancelb = $f->Button
  3355. (-text => 'Cancel',
  3356. -command => sub {
  3357. if ($args{-cancelcommand}) {
  3358. $args{-cancelcommand}->($i);
  3359. }
  3360. $t->destroy;
  3361. },
  3362. )->pack(-side => 'left');
  3363. $cancelb->focus;
  3364. my $deleteb = $f->Button
  3365. (-text => 'Delete',
  3366. -command => sub {
  3367. $project->delete_times($i);
  3368. $update->($t);
  3369. },
  3370. )->pack(-side => 'left');
  3371. $t->bind('<Return>' => sub { $okb->invoke });
  3372. $t->bind('<Escape>' => sub { $cancelb->invoke });
  3373. $t->Popup(-popover => 'cursor');
  3374. };
  3375. my $double_click = sub {
  3376. my $i = shift;
  3377. my $e = $real_lb->XEvent;
  3378. my $x = $e->x;
  3379. my $col_width = 0;
  3380. my $lb_column;
  3381. foreach my $lb_i (0 .. $lb->cget(-columns)-1) {
  3382. my $old_col_width = $col_width;
  3383. $col_width += $lb->columnWidth($lb_i);
  3384. if ($x >= $old_col_width and $x <= $col_width) {
  3385. $lb_column = $lb_i;
  3386. last;
  3387. }
  3388. }
  3389. if ($group eq '' && !$subproj &&
  3390. (!defined $lb_column || $lb_column < 3)) {
  3391. $interval_editor->($i);
  3392. } else {
  3393. $log_viewer->($i);
  3394. }
  3395. };
  3396. $w = $top->Toplevel(-title => "Intervals for " . $project->pathname)
  3397. if !defined $w;
  3398. my $no_cols = 2;
  3399. my $rev_lifetime_col;
  3400. $no_cols ++ if !$group; # zusätzliche To-Spalte
  3401. $no_cols ++ if $group eq 'daily'; # weekday column
  3402. $no_cols ++ if $show_seconds; # für From/Date-Spalte
  3403. $no_cols ++ if $show_seconds and !$group; # für To-Spalte
  3404. $no_cols ++; # Annotations
  3405. $no_cols ++ if $has_costs;
  3406. $no_cols += 2 if @rev;
  3407. $lb = $w->Scrolled('HList', -scrollbars => "oso$sbside",
  3408. -bg => $inner_bg,
  3409. -fg => $inner_fg,
  3410. -columns => $no_cols,
  3411. -width => 80,
  3412. -header => 1,
  3413. -command => $double_click,
  3414. -selectmode => 'extended',
  3415. -exportselection => 1,
  3416. )->pack(-fill => 'both', -expand => 1);
  3417. $w->Advertise(HList => $lb);
  3418. $real_lb = $lb->Subwidget("scrolled");
  3419. {
  3420. my $col = 0;
  3421. if ($group eq 'daily') {
  3422. $lb->header('create', $col++, -text => M"Wkday");
  3423. $lb->header('create', $col++, -text => M"Day");
  3424. } elsif ($group eq 'weekly') {
  3425. $lb->header('create', $col++, -text => M"Week");
  3426. } elsif ($group eq 'monthly') {
  3427. $lb->header('create', $col++, -text => M"Month");
  3428. } else {
  3429. $lb->header('create', $col++, -text => M"From");
  3430. }
  3431. $lb->header('create', $col++, -text => M"seconds")
  3432. if $show_seconds;
  3433. if (!$group) {
  3434. $lb->header('create', $col++, -text => M"To");
  3435. $lb->header('create', $col++, -text => M"seconds")
  3436. if $show_seconds;
  3437. }
  3438. $lb->header('create', $col++, -text => M"Time");
  3439. $lb->header('create', $col++, -text => M"Annotations");
  3440. if ($has_costs) {
  3441. $lb->header('create', $col++, -text => M"Cost");
  3442. }
  3443. if (@rev) {
  3444. $lb->header('create', $col++, -text => M"RCS/CVS");
  3445. $rev_lifetime_col = $col++;
  3446. $lb->header('create', $rev_lifetime_col,
  3447. -text => M"Version lifetime");
  3448. }
  3449. }
  3450. my $anchor_set = 0;
  3451. my $last_rev_def;
  3452. my $i = 0;
  3453. $times = $project->interval_times($group,
  3454. -recursive => $subproj,
  3455. -asref => 1,
  3456. -annotations => 1,
  3457. );
  3458. foreach (@$times) {
  3459. my($from, $to, $annotation, $interval) = @$_;
  3460. my(@fromdate) = localtime($from);
  3461. $fromdate[4]++;
  3462. $fromdate[5]+=1900;
  3463. my $fromdate;
  3464. my $fromwkday;
  3465. if ($group eq '') {
  3466. $fromdate = sprintf "%02d.%02d.%04d %02d:%02d:%02d",
  3467. @fromdate[3,4,5,2,1,0];
  3468. } elsif ($group eq 'daily') {
  3469. $fromwkday = # XXX use POSIX::strftime and locale settings!
  3470. [qw(Sun Mon Tue Wed Thu Fri Sat)]->[$fromdate[6]];
  3471. $fromdate = sprintf("%02d.%02d.%04d",
  3472. @fromdate[3,4,5]
  3473. );
  3474. } elsif ($group eq 'weekly') {
  3475. my $wk = Date::Calc::Week_Number(@fromdate[5,4,3]);
  3476. $fromdate = sprintf "%02d/%04d", $wk, $fromdate[5];
  3477. } elsif ($group eq 'monthly') {
  3478. $fromdate = sprintf "%02d.%04d", @fromdate[4,5];
  3479. } elsif ($group eq 'yearly') {
  3480. $fromdate = sprintf "%04d", $fromdate[5];
  3481. }
  3482. my $col = 0;
  3483. $lb->add($i);
  3484. if ($group eq 'daily') {
  3485. $lb->itemCreate($i, $col++, -text => $fromwkday,
  3486. ($fromwkday =~ /^(Sat|Sun)$/ && $holiday_style # XXX i18n! local holidays!
  3487. ? (-style => $holiday_style)
  3488. : ($weekday_style
  3489. ? (-style => $weekday_style)
  3490. : ()
  3491. )
  3492. )
  3493. );
  3494. }
  3495. $lb->itemCreate($i, $col++, -text => $fromdate);
  3496. $lb->itemCreate($i, $col++, -text => $from)
  3497. if $show_seconds;
  3498. if (!$anchor_set and defined $day and $day <= $from) {
  3499. $lb->anchorSet($i);
  3500. $anchor_set = 1;
  3501. }
  3502. my(@todate, $todate);
  3503. if (defined $to) {
  3504. @todate = localtime($to);
  3505. $todate[4]++;
  3506. $todate[5]+=1900;
  3507. $todate = sprintf
  3508. "%02d.%02d.%04d %02d:%02d:%02d", @todate[3,4,5,2,1,0];
  3509. if (!$group) {
  3510. $lb->itemCreate($i, $col++, -text => $todate);
  3511. $lb->itemCreate($i, $col++, -text => $to)
  3512. if $show_seconds;
  3513. }
  3514. $interval = $to-$from if !$group;
  3515. $lb->itemCreate($i, $col++, -text => sec2time($interval,
  3516. undef, undef));
  3517. if (defined $annotation) {
  3518. # XXX strip annotation to X chars?
  3519. $lb->itemCreate($i, $col, -text => $annotation);
  3520. }
  3521. $col++;
  3522. if ($has_costs) {
  3523. # hier nicht runden, wegen der Granularität...
  3524. my $hours = $interval/3600;
  3525. $lb->itemCreate
  3526. ($i, $col++,
  3527. -text => sprintf("%.2f", $hours*hourly_rate($project)));
  3528. }
  3529. my @t;
  3530. foreach my $rev (@rev) {
  3531. if ($rev->[1] >= $from and $rev->[1] <= $to) {
  3532. my $t = $rev->[0];
  3533. if ($rev->[2]) { $t .= " (" . $rev->[2] . ")" }
  3534. push(@t, $t);
  3535. if (exists $last_rev_def->{'Time'}) {
  3536. $lb->itemCreate
  3537. ($last_rev_def->{'Item'},
  3538. $rev_lifetime_col,
  3539. -text => sec2time($from-$last_rev_def->{'Time'},
  3540. 'dd', 0));
  3541. }
  3542. $last_rev_def = {Time => $to, Item => $i};
  3543. }
  3544. }
  3545. if (@t) { $lb->itemCreate($i, $col++, -text => join(", ", @t)) }
  3546. } else {
  3547. $lb->itemCreate($i, $col++, -text => M"Running");
  3548. }
  3549. $i++;
  3550. }
  3551. #$top->Unbusy($Tk::VERSION >= 800.014 ? (-recurse => 1) : ());
  3552. if (exists $last_rev_def->{'Time'}) {
  3553. $lb->itemCreate
  3554. ($last_rev_def->{'Item'}, $rev_lifetime_col,
  3555. -text => sec2time(time-$last_rev_def->{'Time'}, 'dd', 0));
  3556. }
  3557. my $delete = sub {
  3558. return unless
  3559. lc($lb->messageBox
  3560. (-icon => 'question',
  3561. -title => M"Delete?",
  3562. -message => M"Really delete?",
  3563. -type => M"OkCancel")) eq lc(M"OK");
  3564. $project->delete_times($lb->info('selection'));
  3565. $modified++;
  3566. $update->();
  3567. };
  3568. my $insert = sub {
  3569. my($before_or_after) = @_;
  3570. my @sel = $lb->info('selection');
  3571. my $before;
  3572. if (defined $before_or_after) {
  3573. if ($before_or_after eq 'before') {
  3574. $before = $sel[$#sel]-1;
  3575. } else {
  3576. $before = $sel[$#sel];
  3577. }
  3578. } else {
  3579. $before = (!@sel ? -1 : $sel[$#sel]);
  3580. }
  3581. $project->insert_times_after($before,
  3582. time, time);
  3583. $modified++;
  3584. $interval_editor->($before+1,
  3585. -cancelcommand => sub {
  3586. $project->delete_times($before+1);
  3587. $modified--;
  3588. });
  3589. };
  3590. if (!$group and
  3591. $lb->can("menu") and
  3592. $lb->can("PostPopupMenu")
  3593. and $Tk::VERSION >= 800) {
  3594. my $lb_popup_menu;
  3595. $lb_popup_menu = $lb->Menu(-title => M"Interval menu",
  3596. -disabledforeground => "darkblue",
  3597. -tearoff => 0);
  3598. my $current_index = undef;
  3599. $lb_popup_menu->command(-label => "Interval:",
  3600. -state => "disabled");
  3601. $lb_popup_menu->command
  3602. (-label => M"Edit",
  3603. -command => sub {
  3604. my($index) = $lb->info('selection');
  3605. return if !defined $index;
  3606. $double_click->($index);
  3607. }
  3608. );
  3609. $lb_popup_menu->command
  3610. (-label => M"Delete",
  3611. -command => sub {
  3612. $delete->();
  3613. },
  3614. );
  3615. $lb_popup_menu->command
  3616. (-label => M"Insert before",
  3617. -command => sub {
  3618. $insert->("before");
  3619. },
  3620. );
  3621. $lb_popup_menu->command
  3622. (-label => M"Insert after",
  3623. -command => sub {
  3624. $insert->("after");
  3625. },
  3626. );
  3627. $lb_popup_menu->command
  3628. (-label => M"Move",
  3629. -command => sub {
  3630. # BrowseEntry destroys the selection, so remember as
  3631. # early as possible...
  3632. my(@current_indexes) = $real_lb->info("selection");
  3633. my $t2 = $lb->Toplevel(-title => M"Move interval");
  3634. my $f0 = $t2->Frame->pack(-fill => 'x', -expand => 1);
  3635. $f0->Label(-text => M"New Parent")->pack(-side => "left");
  3636. my $new_project;
  3637. my $be = _all_projects_browseentry
  3638. ($f0, -variable => \$new_project, -excluderoot => 1,
  3639. -exportselection => 0);
  3640. eval { $be->updateListWidth() }; warn $@ if $@;
  3641. $be->pack(-side => "left", -fill => "x", -expand => 1);
  3642. my $f = $t2->Frame->pack(-fill => 'x', -expand => 1);
  3643. my $ok = $f->Button
  3644. (-command => sub {
  3645. my $new_project_p = $root->find_by_pathname($new_project);
  3646. if (!defined $new_project_p) {
  3647. die "Can't find parent $new_project";
  3648. }
  3649. my $new_times = $new_project_p->interval_times("", -asref => 1);
  3650. if (@current_indexes == 0) {
  3651. @current_indexes = $current_index;
  3652. } elsif (@current_indexes == 1) {
  3653. if ($current_index != $current_indexes[0]) {
  3654. warn "Strange mismatch between current_index ($current_index) and current_indexes (@current_indexes)\n";
  3655. @current_indexes = $current_index;
  3656. }
  3657. } else {
  3658. my $yesno = $real_lb->messageBox
  3659. (-message => Mfmt("Really move %d entry/ies to $new_project?", scalar @current_indexes),
  3660. -icon => 'question',
  3661. -title => M"Move",
  3662. -type => 'YesNo',
  3663. );
  3664. if ($yesno !~ /yes/i) {
  3665. return 0;
  3666. }
  3667. }
  3668. # make sure to start from end while processing indexes:
  3669. foreach my $index (sort { $b <=> $a } @current_indexes) {
  3670. $new_project_p->insert_times_after($#$new_times, @{ $times->[$index] });
  3671. $project->delete_times($index);
  3672. }
  3673. $update->();
  3674. # XXX destroy problem?!
  3675. $lb->afterIdle(sub { $t2->destroy if Tk::Exists($t2); });
  3676. })->pack(-side => "left");
  3677. set_text_or_image($ok, "yes.gif", M"OK");
  3678. my $cancel = $f->Button(-command => sub { $t2->destroy() }
  3679. )->pack(-side => 'left');
  3680. $t2->bind('<Escape>' => sub { $cancel->invoke });
  3681. set_text_or_image($cancel, "no.gif", M"Cancel");
  3682. },
  3683. );
  3684. $lb->menu($lb_popup_menu);
  3685. $lb->Subwidget("scrolled")->bind
  3686. ('<3>' => sub {
  3687. my $e = $_[0]->XEvent;
  3688. $lb_popup_menu->entryconfigure(0, -label => "???");
  3689. for my $i (1 .. $lb_popup_menu->index("last")) {
  3690. $lb_popup_menu->entryconfigure($i, -state => "disabled");
  3691. }
  3692. my $y = $e->y;
  3693. $current_index = $lb->nearest($y);
  3694. if (defined $current_index) {
  3695. $lb->anchorSet($current_index);
  3696. if (!$lb->selectionIncludes($current_index)) {
  3697. $lb->selectionClear;
  3698. $lb->selectionSet($current_index);
  3699. }
  3700. my $from = $lb->itemCget($current_index, 0, -text);
  3701. my $to = $lb->itemCget($current_index, 1, -text);
  3702. $lb_popup_menu->entryconfigure(0, -label => "$from - $to");
  3703. for my $i (1 .. $lb_popup_menu->index("last")) {
  3704. $lb_popup_menu->entryconfigure($i, -state => "normal");
  3705. }
  3706. }
  3707. $_[0]->PostPopupMenu($e->X, $e->Y);
  3708. });
  3709. }
  3710. $w->withdraw;
  3711. $lb->see($i-1) if $i > 1;
  3712. my $f = $w->Frame->pack(-fill => 'x');
  3713. $w->Advertise(ButtonFrame => $f);
  3714. my $close_sub = sub {
  3715. #insert_all() if $modified;
  3716. update_project($project) if $modified;
  3717. $w->destroy;
  3718. };
  3719. $w->protocol('WM_DELETE_WINDOW', $close_sub);
  3720. my $clb = $f->Button(-text => M"Close",
  3721. -command => $close_sub,
  3722. )->pack(-side => 'left');
  3723. $f->Label(-text => ' ')->pack(-side => 'left');
  3724. if ($group eq '' && !$subproj) {
  3725. $f->Button(-text => M"Del",
  3726. -command => $delete,
  3727. )->pack(-side => 'left');
  3728. $f->Button(-text => M"Ins",
  3729. -command => sub { $insert->() },
  3730. )->pack(-side => 'left');
  3731. }
  3732. if (!$subproj) {
  3733. $f->Button(-text => M"Re-Sort",
  3734. -command => sub {
  3735. $project->sort_times;
  3736. # $modified++ nicht notwendig, weil sich nichts an der
  3737. # Gesamtzeit ändert
  3738. $update->();
  3739. }
  3740. )->pack(-side => 'left');
  3741. }
  3742. if ($group eq '' && !$subproj) {
  3743. $f->Label(-text => ' ')->pack(-side => 'left');
  3744. $f->Checkbutton(-text => M"Seconds",
  3745. -variable => \$show_seconds,
  3746. -command => sub { $update->() },
  3747. )->pack(-side => 'left');
  3748. }
  3749. # $f->Label(-text => ' ')->pack(-side => 'left');
  3750. require Tk::Optionmenu;
  3751. my $om = $f->Optionmenu(-options => ['',
  3752. [M("daily") => 'daily' ],
  3753. [M("weekly") => 'weekly' ],
  3754. [M("monthly") => 'monthly'],
  3755. [M("yearly") => 'yearly' ],
  3756. ],
  3757. )->pack(-side => "right");
  3758. # Hack for buggy Tk::Optionmenu in Tk804:
  3759. $om->configure(-variable => \$group,
  3760. -textvariable => \$group,
  3761. );
  3762. $f->Label(-text => " " . M"Group:")->pack(-side => 'right');
  3763. $f->Checkbutton(-text => M"Subprojects",
  3764. -variable => \$subproj,
  3765. -command => sub { $update->() },
  3766. )->pack(-side => 'right');
  3767. # -command cannot be specified at creation time, because this can
  3768. # cause endless loops, at least in Tk 800.023
  3769. $f->afterIdle(sub {$om->configure(-command => sub { $update->() })});
  3770. $clb->focus;
  3771. $w->bind('<Escape>' => sub { $clb->invoke });
  3772. my @popup_args;
  3773. #push @popup_args, (-popover => 'cursor') unless $geometry;
  3774. $w->Popup; #(@popup_args);
  3775. if ($geometry) {
  3776. $w->geometry($geometry);
  3777. }
  3778. }
  3779. sub show_note {
  3780. my($top, $project, %args) = @_;
  3781. if (!defined $project) {
  3782. $project = entry_to_project(get_sel_entry());
  3783. return if !defined $project;
  3784. }
  3785. my $t = $top->Toplevel(-title => M('Note for').' '.$project->pathname);
  3786. my $txt = $t->Scrolled('Text', -scrollbars => "so$sbside"
  3787. )->pack(-fill => 'both', -expand => 1);
  3788. $txt->focus;
  3789. if ($project->has_note) {
  3790. foreach ($project->note) {
  3791. $txt->insert('end', $_ . "\n");
  3792. }
  3793. }
  3794. my $f = $t->Frame->pack(-fill => 'x', -expand => 1);
  3795. $f->Button(-text => M"OK",
  3796. -command => sub {
  3797. my $s = $txt->get('1.0', 'end');
  3798. $project->set_note(split(/\n/, $s));
  3799. $t->destroy;
  3800. })->pack(-side => 'left');
  3801. my $cancel = $f->Button(-text => M"Cancel",
  3802. -command => sub { $t->destroy })->pack(-side => 'left');
  3803. $t->bind('<Escape>' => sub { $cancel->invoke });
  3804. $t->Popup(-popover => 'cursor');
  3805. }
  3806. sub set_time_update {
  3807. my $old_time_update = $time_update;
  3808. $time_update = ($options->{'dateformat'} eq 'hs' ? 1 : 60);
  3809. $time_update < $old_time_update;
  3810. }
  3811. sub set_text_or_image {
  3812. my($widget, $image, $text) = @_;
  3813. # use image if available, otherwise text
  3814. if (-r $image) {
  3815. eval { $widget->configure
  3816. (-image => $widget->Photo(-file => Tk::findINC($image)))
  3817. };
  3818. if (!$@) { return }
  3819. }
  3820. $widget->configure(-text => $text);
  3821. }
  3822. sub make_path {
  3823. my($p) = @_;
  3824. return if !$p;
  3825. die Mfmt("wrong arg for make_path: <%s>",$p) if !$p->can('Timex_Project_API');
  3826. my @path = $p->path;
  3827. join $separator, @path[1 .. $#path];
  3828. }
  3829. sub get_parent_path {
  3830. my $path = shift;
  3831. my @path = split "\Q$separator\E", $path;
  3832. join $separator, @path[0 .. $#path-1];
  3833. }
  3834. sub get_entry {
  3835. my($w) = @_;
  3836. my $Ev = $w->XEvent;
  3837. $w->GetNearest($Ev->y);
  3838. }
  3839. sub get_sel_entry {
  3840. my $path = $project_frame->info('anchor');
  3841. return $path if defined $path;
  3842. ($project_frame->info('selection'))[0];
  3843. }
  3844. sub entry_to_project {
  3845. my($path) = @_;
  3846. return if !defined $path;
  3847. $project_frame->info('data', $path);
  3848. }
  3849. sub quit_program {
  3850. my $non_interactive = shift;
  3851. if (!$non_interactive) {
  3852. require Tk::Dialog;
  3853. if ($root->modified || defined $current_project) {
  3854. if (!defined $quit_dialog) {
  3855. $quit_dialog = $top->Dialog
  3856. (-title => M"Quit Program",
  3857. -text => M("Really quit?\n") .
  3858. ($root->modified ?
  3859. M("(modified data) ") : "") .
  3860. (defined $current_project ?
  3861. M("(project running) ") : ""),
  3862. -default_button => M"No",
  3863. -buttons => [M"Yes", M"No"],
  3864. -popover => 'cursor',
  3865. );
  3866. }
  3867. return 0 if $quit_dialog->Show ne M"Yes";
  3868. }
  3869. }
  3870. $top->destroy;
  3871. }
  3872. sub sec2time {
  3873. my($sec, $dateformat, $day8) = @_;
  3874. $dateformat = $options->{'dateformat'} unless defined $dateformat;
  3875. $day8 = $options->{'day8'} unless defined $day8;
  3876. my($day, $hour, $min);
  3877. if ($dateformat =~ /^d/) {
  3878. $day = int($sec / ($day8 ? 28800 : 86400));
  3879. $sec = $sec % ($day8 ? 28800 : 86400);
  3880. } elsif ($dateformat eq 'frac d') {
  3881. $day = $sec / ($day8 ? 28800 : 86400);
  3882. }
  3883. if ($dateformat eq 'frac h') {
  3884. $hour = $sec / 3600;
  3885. } else {
  3886. $hour = int($sec / 3600);
  3887. $sec = $sec % 3600;
  3888. $min = int($sec / 60);
  3889. }
  3890. if ($dateformat eq 'd') {
  3891. sprintf("%3dd %02d:%02d", $day, $hour, $min);
  3892. } elsif ($dateformat eq 'h') {
  3893. sprintf("%3d:%02d", $hour, $min);
  3894. } elsif ($dateformat eq 'dd') { # round working days
  3895. sprintf("%3dd", $day + ($hour >= ($day8 ? 4 : 12) ? 1 : 0));
  3896. } elsif ($dateformat eq 'frac d') {
  3897. sprintf("%.2fd", $day);
  3898. } elsif ($dateformat eq 'frac h') {
  3899. sprintf("%.2fh", $hour);
  3900. } else {
  3901. sprintf("%02d:%02d:%02d", $hour, $min, $sec % 60);
  3902. }
  3903. }
  3904. sub check_still_today {
  3905. my @new_nowtime = localtime;
  3906. my $new_today_time =
  3907. time - $new_nowtime[0] - $new_nowtime[1]*60 - $new_nowtime[2]*60*60;
  3908. if ($new_today_time != $today_time) {
  3909. $today_time = $new_today_time;
  3910. @nowtime = @new_nowtime;
  3911. insert_all();
  3912. }
  3913. }
  3914. # force appending extension (default: .pj1) to filename
  3915. sub adjust_filename {
  3916. my($file, $ext) = @_;
  3917. $ext = ".pj1" unless defined $ext;
  3918. (my $ext_re = $ext) =~ s/\./\\./g; # quote dots for regex
  3919. if ($file !~ /$ext_re$/) {
  3920. $file = "$file$ext";
  3921. }
  3922. $file;
  3923. }
  3924. sub create_menu_last_projects {
  3925. # find last separator
  3926. my $end = $mb_file_menu->index('end');
  3927. my $i = $end;
  3928. LOOP: {
  3929. while ($i >= 0) {
  3930. last LOOP if ($mb_file_menu->type($i) eq 'separator');
  3931. $i--;
  3932. }
  3933. $status_text->configure(M"Separator in Menu File not found");
  3934. return;
  3935. }
  3936. # delete anything from the item after the separator to the end
  3937. if ($i < $end) {
  3938. $mb_file_menu->delete($i+1, 'end');
  3939. }
  3940. # insert last_projects
  3941. $i = 0;
  3942. foreach my $p (@$last_projects) {
  3943. my $pathname = $p->pathname;
  3944. $i++;
  3945. $mb_file_menu->command(-label => "$i: " . $pathname,
  3946. -underline => 0,
  3947. -command => sub {
  3948. start($p);
  3949. });
  3950. }
  3951. }
  3952. sub add_last_projects {
  3953. my($project) = @_;
  3954. my $i;
  3955. for($i = 0; $i <= $#$last_projects; $i++) {
  3956. if ($last_projects->[$i] eq $project) {
  3957. splice @$last_projects, $i, 1;
  3958. last;
  3959. }
  3960. }
  3961. unshift(@$last_projects, $project);
  3962. if (@$last_projects > $max_last_projects) {
  3963. $#$last_projects = $max_last_projects-1; # $max_last_projects Dateien merken
  3964. }
  3965. }
  3966. # XXX bei KDE gibt es das Problem, daß beim ersten Minimize
  3967. # das Fenster nach +0+0 springt ... fvwm2 hat damit keine Probleme (?)
  3968. sub minmaximze {
  3969. $minimized = !$minimized;
  3970. if ($minimized) {
  3971. $min_button->configure(-image => $down_photo);
  3972. $balloon->attach($min_button, -msg => 'Maximize')
  3973. if $balloon;
  3974. $save_geometry = $top->Width . "x" . $top->Height;
  3975. my $menu_height = $top->Height
  3976. - $project_frame->Height - $status_frame->Height;
  3977. $top->geometry($top->Width . "x" . $menu_height);
  3978. } else {
  3979. $min_button->configure(-image => $up_photo);
  3980. $balloon->attach($min_button, -msg => 'Minimize')
  3981. if $balloon;
  3982. $top->geometry($save_geometry);
  3983. $top->raise;
  3984. }
  3985. }
  3986. sub accept_drop {
  3987. my($w, $seln) = @_;
  3988. my $filename;
  3989. eval {
  3990. my @targ = $w->SelectionGet('-selection'=>$seln,'TARGETS');
  3991. foreach (@targ) {
  3992. if (/FILE_NAME/) {
  3993. $filename = $w->SelectionGet('-selection'=>$seln,$_);
  3994. last;
  3995. } elsif ($Tk::platform eq 'MSWin32' && /STRING/) {
  3996. $filename = $w->SelectionGet('-selection'=>$seln,$_);
  3997. last;
  3998. } elsif (/text\/uri-list/) { # gmc Xdnd
  3999. $filename = join "", map { chr } $w->SelectionGet('-selection'=>$seln,$_);
  4000. $filename =~ s/\0$//;
  4001. $filename = (split /\015\012/, $filename)[0];
  4002. $filename =~ s/^file://;
  4003. last;
  4004. }
  4005. }
  4006. };
  4007. if ($@) {
  4008. # Konqueror 2 Xdnd
  4009. $filename = $w->SelectionGet('-selection'=>$seln);
  4010. $filename =~ s/^file://;
  4011. }
  4012. if (defined $filename) {
  4013. $w->after(10, sub {load_merge_popup($filename)});
  4014. }
  4015. }
  4016. sub load_merge_popup {
  4017. my $filename = shift;
  4018. $load_merge_filename = $filename;
  4019. if (!Tk::Exists($load_menu)) {
  4020. $load_menu = $top->Menu(-tearoff => 0);
  4021. $load_menu->command(-label => M"Merge",
  4022. -command => sub {
  4023. merge_file_noninteractive($filename);
  4024. });
  4025. $load_menu->command(-label => M"Load",
  4026. -command => sub {
  4027. load_file_noninteractive($filename);
  4028. });
  4029. $load_menu->command(-label => M"Cancel",
  4030. -command => sub { });
  4031. }
  4032. $load_menu->Post($top->pointerx, $top->pointery);
  4033. }
  4034. sub get_home_dir {
  4035. if (!defined $home) {
  4036. if ($^O eq 'MSWin32') {
  4037. eval q{
  4038. use Win32Util;
  4039. $home = Win32Util::get_user_folder();
  4040. };
  4041. } else {
  4042. $home = eval q{
  4043. local $SIG{__DIE__};
  4044. (getpwuid($<))[7];
  4045. };
  4046. }
  4047. if (!defined $home) {
  4048. $home = $ENV{'HOME'} || '/';
  4049. }
  4050. }
  4051. $home;
  4052. }
  4053. sub get_user_name {
  4054. $username = $options->{username};
  4055. if (!defined $username || $username =~ m{^\s*$}) {
  4056. if ($^O eq 'MSWin32') {
  4057. eval q{
  4058. use Win32Util;
  4059. $username = Win32Util::get_user_name();
  4060. };
  4061. } else {
  4062. $username = eval q{
  4063. local $SIG{__DIE__};
  4064. getpwuid($<))[0];
  4065. };
  4066. }
  4067. if (!defined $username || $username =~ m{^\s*$}) {
  4068. $username = $ENV{USERNAME} || $ENV{USER} || "";
  4069. }
  4070. }
  4071. $username;
  4072. }
  4073. sub get_real_name {
  4074. $realname = $options->{realname};
  4075. if (!defined $realname || $realname =~ m{^\s*$}) {
  4076. $realname = eval q{
  4077. local $SIG{__DIE__};
  4078. ((getpwuid($<))[6]);
  4079. };
  4080. $realname =~ s/,.*//;
  4081. }
  4082. $realname;
  4083. }
  4084. # This is a hack using xwininfo to report if another tktimex window
  4085. # is already running. This must be called before $top is created...
  4086. sub tktimex_running {
  4087. return 0 if ($os eq 'win');
  4088. open(WININFO, "xwininfo -tree -root |");
  4089. my $r = 0;
  4090. while (<WININFO>) {
  4091. if (/^\s*0x[0-9a-fA-F]+\s+"tktimex.*":\s+\("tktimex"\s+"Tktimex"\)/) {
  4092. $r = 1;
  4093. last;
  4094. }
  4095. }
  4096. close WININFO;
  4097. return $r;
  4098. }
  4099. sub get_rcs_from_cache {
  4100. my $project = shift;
  4101. my $rcs;
  4102. if ($rcs_cache{$project->rcsfile}) {
  4103. $rcs = $rcs_cache{$project->rcsfile};
  4104. } else {
  4105. require Timex::Rcs;
  4106. $rcs = new Timex::Rcs $project->rcsfile;
  4107. $rcs_cache{$project->rcsfile} = $rcs;
  4108. }
  4109. $rcs;
  4110. }
  4111. sub hourly_rate {
  4112. my $p = shift;
  4113. my $rate = $p->rate;
  4114. $rate = $options->{'hourlyrate'} if (!defined $rate);
  4115. $rate;
  4116. }
  4117. sub pi () { 4 * atan2(1, 1) } # 3.141592653
  4118. sub deg2rad { ($_[0]*pi)/180 }
  4119. # HList hack... XXX move to MyHList XXX
  4120. sub MyButtonRelease1
  4121. {
  4122. my ($w) = @_;
  4123. my $Ev = $w->XEvent;
  4124. delete $w->{'shiftanchor'};
  4125. my $mode = $w->cget('-selectmode');
  4126. if($mode eq 'dragdrop')
  4127. {
  4128. # $w->Send_DoneDrag();
  4129. return;
  4130. }
  4131. my ($x, $y) = ($Ev->x, $Ev->y);
  4132. my $ent = $w->GetNearest($y, 1);
  4133. if (!defined($ent) and $mode eq 'single')
  4134. {
  4135. my($ent) = $w->info('selection');
  4136. if (defined $ent)
  4137. {
  4138. $w->anchorSet($ent);
  4139. }
  4140. }
  4141. return unless (defined($ent) and length($ent));
  4142. if(exists $w->{tixindicator})
  4143. {
  4144. return unless delete($w->{tixindicator}) eq $ent;
  4145. my @info = $w->info('item',$Ev->x, $Ev->y);
  4146. if(defined($info[1]) && $info[1] eq 'indicator')
  4147. {
  4148. $w->Callback(-indicatorcmd => $ent, '<Activate>');
  4149. }
  4150. return;
  4151. }
  4152. if($mode eq 'single' || $mode eq 'browse')
  4153. {
  4154. $w->anchorSet($ent);
  4155. }
  4156. Tk->break;
  4157. }
  4158. sub project_status {
  4159. M("Left: Select | Middle: ") .
  4160. ($options->{'autoscroll'} !~ /^(|none)$/
  4161. ? M"Scroll"
  4162. : M"Create Subproject"
  4163. ) .
  4164. M" | Right: Attributes";
  4165. }
  4166. sub notimes_check {
  4167. if ($root->notimes) {
  4168. $top->messageBox(-icon => "warning",
  4169. -message => M"No times available",
  4170. );
  4171. die;
  4172. }
  4173. }
  4174. sub show_about {
  4175. my $dia = $top->Toplevel(-title => M"Copyright");
  4176. # XXX Übersetung?
  4177. $dia->Label(-text => <<EOF,
  4178. tktimex $VERSION
  4179. Tk $Tk::VERSION
  4180. perl $]
  4181. For copyright see Help > Copyright
  4182. EOF
  4183. -justify => 'left')->pack;
  4184. my $okb = $dia->Button(-text => M"OK",
  4185. -command => sub { $dia->destroy })->pack;
  4186. $okb->focus;
  4187. $dia->bind('<Escape>' => sub { $okb->invoke });
  4188. $dia->Popup(-popover => 'cursor');
  4189. }
  4190. sub show_copyright {
  4191. my $dia = $top->Toplevel(-title => M"Copyright");
  4192. # XXX Übersetung?
  4193. $dia->Label(-text => <<'EOF',
  4194. tktimex by Slaven Rezic (eserte@users.sourceforge.net)
  4195. Copyright (c) 1996-2005 Slaven Rezic. All rights reserved.
  4196. Redistribution and use in source and binary forms, with or without
  4197. modification, are permitted provided that the following conditions
  4198. are met:
  4199. 1. Redistributions of source code must retain the above copyright
  4200. notice, this list of conditions and the following disclaimer.
  4201. 2. Redistributions in binary form must reproduce the above copyright
  4202. notice, this list of conditions and the following disclaimer in the
  4203. documentation and/or other materials provided with the distribution.
  4204. 3. All advertising materials mentioning features or use of this software
  4205. must display the following acknowledgement:
  4206. This product includes software developed by Slaven Rezic.
  4207. 4. The name of the author may not be used to endorse or promote products
  4208. derived from this software without specific prior written permission.
  4209. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  4210. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  4211. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  4212. ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  4213. FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  4214. DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  4215. OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  4216. HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  4217. LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  4218. OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  4219. SUCH DAMAGE.
  4220. EOF
  4221. -justify => 'left')->pack;
  4222. my $okb = $dia->Button(-text => M"OK",
  4223. -command => sub { $dia->destroy })->pack;
  4224. $okb->focus;
  4225. $dia->bind('<Escape>' => sub { $okb->invoke });
  4226. $dia->Popup(-popover => 'cursor');
  4227. }
  4228. =head1 NAME
  4229. tktimex - time recording tool
  4230. =head1 SYNOPSIS
  4231. tktimex [options] [projectfile]
  4232. =head1 DESCRIPTION
  4233. B<tktimex> is a time recording tool. Its purpose is to record working
  4234. times for projects. Projects may be grouped hierarchically with
  4235. subprojects. It is also possible to get some daily/weekey/monthly
  4236. statistics.
  4237. =head2 QUICK OVERVIEW
  4238. To create a new project, select from the B<Project> menu the item
  4239. B<New>.
  4240. To start the timer on a particular project, select the project from
  4241. the list by mouse click and click on the B<Cont> button. To stop the
  4242. timer, click on the B<Pause> button. You can also double-click on a
  4243. project to start/stop the timer.
  4244. If autosaving is on (which is the default), after each click on
  4245. B<Pause>, the project list will be updated on disk, and so will every
  4246. 10 minutes. If autosaving is off, you have to manually save the
  4247. project list by clicking on the B<Save> button.
  4248. To reload an project list file, you have to specify the file name on
  4249. the command line:
  4250. tktimex projectfile.pj1
  4251. If Tk::Getopt is installed on your system (highly recommended!), you
  4252. can set the default project list file in the B<Option editor> (menu
  4253. B<Options>).
  4254. =head1 COMMAND LINE OPTIONS
  4255. Possible options are:
  4256. --file
  4257. --mergedir
  4258. --[no]lock (default: 1)
  4259. --[no]one-instance
  4260. --[no]as, --[no]autosave (default: 1)
  4261. --update (default: 600)
  4262. --[no]oneday-immediately (default: 1)
  4263. --geometry (default: 500x230)
  4264. --[no]securesave
  4265. --enterpriseprojects
  4266. --enterprisedefaults
  4267. --df, --dateformat (default: h)
  4268. --[no]day8 (default: 1)
  4269. --[no]archived
  4270. --[no]onlytop
  4271. --domain
  4272. --sort (default: name)
  4273. --[no]busyind
  4274. --autoscroll (default: none)
  4275. --hourlyrate
  4276. --currency (default: EUR)
  4277. =head1 TODO
  4278. - better Pod
  4279. - enterprice-wide settings:
  4280. - default getopt settings
  4281. - central repository for user data (this directory should be 4777
  4282. or 4555 with all the files already created)
  4283. - template sets
  4284. - set of all projects running in system
  4285. - private vs. enterprice projects
  4286. =head1 BUGS
  4287. If tktimex crashes (it should only due to perl/Tk or OS problems!),
  4288. then it is possible that the project file gets corrupted. To prevent
  4289. loss of data, there are always some backup files with the suffixes .1,
  4290. .2 etc.
  4291. The -oneday-immediately option is not supported with Tk::DateEntry.
  4292. Setting dateformat to "hs" (show hours, minutes and seconds) is not
  4293. recommended due to cpu waste. Better leave the option at "d" or "h".
  4294. =head1 FILES
  4295. ~/.tktimexrc personal configuration file
  4296. ~/.tktimex.last list of last accessed projects
  4297. *.pj1 project files
  4298. =head1 SEE ALSO
  4299. L<perl>, L<Tk>, L<rcsintro(1)>, L<cvs(1)>, L<Timex::Project>
  4300. =head1 AUTHOR
  4301. Slaven Rezic (eserte@users.sourceforge.net)
  4302. Copyright (c) 1996-2005 Slaven Rezic. All rights reserved.
  4303. For a complete copyright see the Help/About menu entry.
  4304. =cut