/tktimex

https://github.com/gitpan/Timex · Perl · 4800 lines · 4169 code · 496 blank · 135 comment · 602 complexity · be8a40721fbb4ea810af4c6fcad77528 MD5 · raw file

Large files are truncated click here to view the full 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',