class Openall_time_applet::Gui::Win_main attr_reader :args, :gui def initialize(args) @args = args @gui = Gtk::Builder.new.add("#{File.dirname(__FILE__)}/../glade/win_main.glade") @gui.translate @gui.connect_signals{|h| method(h)} @gui["window"].icon = "#{File.dirname(__FILE__)}/../gfx/icon_time_black.png" #Generate list-store containing tasks for the task-column. task_ls = Gtk::ListStore.new(String, String) iter = task_ls.append iter[0] = _("None") iter[1] = 0.to_s tasks = [_("Choose:")] @args[:oata].ob.list(:Task, "orderby" => "title") do |task| iter = task_ls.append iter[0] = task[:title] iter[1] = task.id.to_s tasks << task end #Set up completion for description entry. @descr_ec = Gtk::EntryCompletion.new @descr_ec.model = Gtk::ListStore.new(String) @descr_ec.text_column = 0 @gui["txtDescr"].completion = @descr_ec self.reload_descr_completion @descr_ec.signal_connect("match-selected") do |me, model, iter| text = model.get_value(iter, 0) me.entry.text = text true end #Add the tasks to the combo-box. @gui["cbTask"].init(tasks) init_data = @gui["tvTimelogs"].init([ _("ID"), { :title => _("Timestamp"), :type => :string, :markup => true, :expand => false }, { :title => _("Time"), :type => :string, :markup => true, :expand => false }, { :title => _("Description"), :type => :string, :markup => true, :expand => true }, { :title => _("T-time"), :type => :string, :markup => true, :expand => false }, { :title => _("T-km"), :type => :string, :markup => true }, { :title => _("T-Descr."), :type => :string, :markup => true, :expand => true }, { :title => _("Cost"), :type => :string, :markup => true }, { :title => _("Fixed"), :type => :toggle, :expand => false }, { :title => _("Internal"), :type => :toggle, :expand => false }, { :title => _("Task"), :type => :combo, :model => task_ls, :has_entry => false, :markup => true, :expand => true } ]) Knj::Gtk2::Tv.editable_text_renderers_to_model( :ob => @args[:oata].ob, :tv => @gui["tvTimelogs"], :model_class => :Timelog, :renderers => init_data[:renderers], :change_before => proc{|d| if d[:col_no] == 3 and @args[:oata].timelog_active and @args[:oata].timelog_active.id == d[:model].id raise _("You cannot edit the time for the active timelog.") end @dont_reload = true }, :change_after => proc{ @dont_reload = false }, :on_edit => proc{|d| @tv_editting = true }, :on_edit_done => proc{|d| @tv_editting = nil }, :cols => { 1 => { :col => :timestamp, :value_callback => self.method(:tv_editable_timestamp_callback), :value_set_callback => self.method(:tv_editable_timestamp_set_callback) }, 2 => { :col => :time, :value_callback => proc{ |data| Knj::Strings.human_time_str_to_secs(data[:value]) }, :value_set_callback => proc{ |data| Knj::Strings.secs_to_human_time_str(data[:value], :secs => false) } }, 3 => :descr, 4 => { :col => :time_transport, :value_callback => proc{ |data| Knj::Strings.human_time_str_to_secs(data[:value]) }, :value_set_callback => proc{ |data| Knj::Strings.secs_to_human_time_str(data[:value], :secs => false) } }, 5 => {:col => :transportlength, :type => :int}, 6 => {:col => :transportdescription}, 7 => {:col => :transportcosts, :type => :human_number, :decimals => 2}, 8 => {:col => :travelfixed}, 9 => {:col => :workinternal}, 10 => { :col => :task_id, :value_callback => lambda{|data| task = @args[:oata].ob.get_by(:Task, {"title" => data[:value]}) if !task return 0 else return task.id end }, :value_set_callback => proc{|data| data[:model].task_name } } } ) #The ID column should not be visible (it is only used to identify which timelog the row represents). @gui["tvTimelogs"].columns[0].visible = false #Connect certain column renderers to the editingStarted-method, so editing can be canceled, if the user tries to edit forbidden data on the active timelog. init_data[:renderers][3].signal_connect_after("editing-started", :time, &self.method(:on_cell_editingStarted)) #Fills the timelogs-treeview with data. self.reload_timelogs #Reload the treeview if something happened to a timelog. @reload_id = @args[:oata].ob.connect("object" => :Timelog, "signals" => ["add", "update", "delete"], &self.method(:reload_timelogs)) #Update switch-button. self.update_switch_button #Update switch-button when active timelog is changed. @event_timelog_active_changed = @args[:oata].events.connect(:timelog_active_changed) do self.update_switch_button self.check_rows self.timelog_info_trigger end #This timeout controls the updating of the timelog-info-frame and the time-counter for the active timelog in the treeview. @timeout_id = Gtk.timeout_add(1000) do self.check_rows self.timelog_info_trigger true end #Initializes sync-box. @gui["btnSyncPrepareTransfer"].label = _("Transfer") #Generate list-store containing tasks for the task-column. task_ls = Gtk::ListStore.new(String, String) iter = task_ls.append iter[0] = _("None") iter[1] = 0.to_s tasks = [_("Choose:")] @args[:oata].ob.list(:Task, {"orderby" => "title"}) do |task| iter = task_ls.append iter[0] = task[:title] iter[1] = task.id.to_s tasks << task end #Initialize timelog treeview. init_data = Knj::Gtk2::Tv.init(@gui["tvTimelogsPrepareTransfer"], [ _("ID"), { :title => _("Description"), :type => :string, :expand => true }, _("Timestamp"), _("Time"), _("T-time"), _("T-km"), { :title => _("T-Descr."), :type => :string, :expand => true }, _("Cost"), { :title => _("Fixed"), :type => :toggle }, { :title => _("Internal"), :type => :toggle }, { :title => _("Skip"), :type => :toggle }, { :title => _("Task"), :type => :combo, :model => task_ls, :has_entry => false, :expand => true }, _("Sync time") ]) #Make columns editable. Knj::Gtk2::Tv.editable_text_renderers_to_model( :ob => @args[:oata].ob, :tv => @gui["tvTimelogsPrepareTransfer"], :model_class => :Timelog, :renderers => init_data[:renderers], :change_before => proc{ @dont_reload_sync = true }, :change_after => proc{ @dont_reload_sync = false; self.update_sync_totals }, :cols => { 1 => :descr, 2 => { :col => :timestamp, :value_callback => self.method(:tv_editable_timestamp_callback), :value_set_callback => self.method(:tv_editable_timestamp_set_callback) }, 3 => { :col => :time, :value_callback => proc{ |data| Knj::Strings.human_time_str_to_secs(data[:value]) }, :value_set_callback => proc{ |data| Knj::Strings.secs_to_human_time_str(data[:value], :secs => false) } }, 4 => { :col => :time_transport, :value_callback => proc{ |data| Knj::Strings.human_time_str_to_secs(data[:value]) }, :value_set_callback => proc{ |data| Knj::Strings.secs_to_human_time_str(data[:value], :secs => false) } }, 5 => {:col => :transportlength, :type => :int}, 6 => {:col => :transportdescription}, 7 => {:col => :transportcosts, :type => :human_number, :decimals => 2}, 8 => {:col => :travelfixed}, 9 => {:col => :workinternal}, 10 => {:col => :sync_need, :type => :toggle_rev}, 11 => { :col => :task_id, :value_callback => lambda{ |data| task = @args[:oata].ob.get_by(:Task, {"title" => data[:value]}) if !task return 0 else return task.id end }, :value_set_callback => proc{ |data| data[:model].task_name } }, 12 => { :col => :time_sync, :value_callback => proc{ |data| Knj::Strings.human_time_str_to_secs(data[:value]) }, :value_set_callback => proc{ |data| Knj::Strings.secs_to_human_time_str(data[:value], :secs => false) } } } ) @gui["tvTimelogsPrepareTransfer"].columns[0].visible = false @gui["vboxPrepareTransfer"].hide @reload_preparetransfer_id = @args[:oata].ob.connect("object" => :Timelog, "signals" => ["add", "update", "delete"], &self.method(:reload_timelogs)) #Show the window. @gui["window"].show self.timelog_info_trigger width = @gui["window"].size[0] @gui["window"].resize(width, 1) end def tv_editable_timestamp_callback(data) if match = data[:value].match(/^\s*(\d+)\s*:\s*(\d+)\s*$/) date = Datet.new date.hour = match[1].to_i date.min = match[2].to_i return date elsif match = data[:value].match(/^\s*(\d+)\s*\/\s*(\d+)\s*$/) date = data[:model].timestamp date.day = match[1].to_i date.month = match[2].to_i return date else return Datet.in(data[:value]) end end def tv_editable_timestamp_set_callback(data) date = Datet.in(data[:value]) if date.strftime("%Y %m %d") == Time.now.strftime("%Y %m %d") return date.strftime("%H:%M") else return date.strftime("%d/%m") end end #Reloads the suggestions for the description-entry-completion. def reload_descr_completion added = {} @descr_ec.model.clear @args[:oata].ob.list(:Worktime, "orderby" => "timestamp") do |worktime| next if added.key?(worktime[:comment]) added[worktime[:comment]] = true @descr_ec.model.append[0] = worktime[:comment] end @args[:oata].ob.list(:Timelog, "orderby" => "descr") do |timelog| next if added.key?(timelog[:descr]) added[timelog[:descr]] = true @descr_ec.model.append[0] = timelog[:descr] end end def reload_timelogs_preparetransfer return nil if @dont_reload_sync or @gui["tvTimelogsPrepareTransfer"].destroyed? @gui["tvTimelogsPrepareTransfer"].model.clear @timelogs_sync_count = 0 tnow_str = Time.now.strftime("%Y %m %d") @args[:oata].ob.list(:Timelog, "task_id_not" => ["", 0], "orderby" => "timestamp") do |timelog| #Read time and transport from timelog. time = timelog[:time].to_i transport = timelog[:time_transport].to_i #If transport is logged, then the work if offsite. It should be rounded up by 30 min. Else 15 min. round up. if transport > 0 roundup = 1800 else roundup = 900 end #Do the actual counting. count_rounded_time = 0 loop do break if count_rounded_time >= time count_rounded_time += roundup end #Set sync-time on timelog. timelog[:time_sync] = count_rounded_time tstamp = timelog.timestamp if tstamp.strftime("%Y %m %d") == tnow_str tstamp_str = tstamp.strftime("%H:%M") else tstamp_str = tstamp.strftime("%d/%m") end Knj::Gtk2::Tv.append(@gui["tvTimelogsPrepareTransfer"], [ timelog.id, timelog[:descr], tstamp_str, timelog.time_as_human, timelog.time_transport_as_human, Knj::Locales.number_out(timelog[:transportlength], 0), timelog.transport_descr_short, Knj::Locales.number_out(timelog[:transportcosts], 2), Knj::Strings.yn_str(timelog[:travelfixed], true, false), Knj::Strings.yn_str(timelog[:workinternal], true, false), Knj::Strings.yn_str(timelog[:sync_need], false, true), timelog.task_name, Knj::Strings.secs_to_human_time_str(count_rounded_time, :secs => false) ]) @timelogs_sync_count += 1 end end def on_btnSync_clicked self.reload_timelogs_preparetransfer if @timelogs_sync_count <= 0 #Show error-message and destroy the window, if no timelogs was to be synced. Knj::Gtk2.msgbox("msg" => _("There is nothing to sync at this time."), "run" => false) else #...else show the window. @gui["expOverview"].hide self.update_sync_totals @gui["vboxPrepareTransfer"].show end end def update_sync_totals total_secs = 0 @gui["tvTimelogsPrepareTransfer"].model.each do |model, path, iter| time_val = @gui["tvTimelogsPrepareTransfer"].model.get_value(iter, 12) begin total_secs += Knj::Strings.human_time_str_to_secs(time_val) rescue #ignore - user is properly entering stuff. end end @gui["labTotal"].markup = "#{_("Total hours:")} #{Knj::Strings.secs_to_human_time_str(total_secs, :secs => false)}" end def on_btnCancelPrepareTransfer_clicked @gui["expOverview"].show @gui["vboxPrepareTransfer"].hide end #This method is called, when editting starts in a description-, time- or task-cell. If it is the active timelog, then editting is canceled. def on_cell_editingStarted(renderer, editable, path, col_title) iter = @gui["tvTimelogs"].model.get_iter(path) timelog_id = @gui["tvTimelogs"].model.get_value(iter, 0).to_i if tlog = @args[:oata].timelog_active and tlog.id.to_i == timelog_id renderer.stop_editing(true) Knj::Gtk2.msgbox(_("You cannot edit this on the active timelog.")) end end #This method is used to do stuff without having the treeview reloading. It executes the given block and then makes the treeview reloadable again. def dont_reload @dont_reload = true begin yield ensure @dont_reload = false end end #Removes all timelogs from the treeview and adds them again. Does nothing if the 'dont_reload'-variable is set. def reload_timelogs self.reload_descr_completion return nil if @dont_reload or @gui["tvTimelogs"].destroyed? tnow_str = Time.now.strftime("%Y %m %d") @gui["tvTimelogs"].model.clear @args[:oata].ob.list(:Timelog, "orderby" => ["task_id", "descr", "timestamp"]) do |timelog| begin tstamp_str = timelog.timestamp_str rescue => e tstamp_str = "[#{_("error")}: #{e.message}" end tstamp = timelog.timestamp if tstamp.strftime("%Y %m %d") == tnow_str tstamp_str = tstamp.strftime("%H:%M") else tstamp_str = tstamp.strftime("%d/%m") end @gui["tvTimelogs"].append([ timelog.id, tstamp_str, timelog.time_as_human, Knj::Web.html(timelog[:descr]), timelog.time_transport_as_human, Knj::Locales.number_out(timelog[:transportlength], 0), Knj::Web.html(timelog[:transportdescription]), Knj::Locales.number_out(timelog[:transportcosts], 2), Knj::Strings.yn_str(timelog[:travelfixed], true, false), Knj::Strings.yn_str(timelog[:workinternal], true, false), timelog.task_name ]) end #Reset cache of which rows are set to bold. @bold_rows = {} end def on_imiQuit_activate #Check if a timelog needs to be synced. If so the user needs to confirm he really wants to quit. timelog_found = nil do_destroy = true @args[:oata].ob.list(:Timelog, "task_id_not" => ["", 0]) do |timelog| if timelog.time_total > 0 or timelog.time_total(:transport => true) > 0 or timelog[:sync_need].to_i == 1 timelog_found = timelog break end end if timelog_found if Knj::Gtk2.msgbox(sprintf(_("The timelog '%s' has not been synced. Are you sure you want to quit?"), timelog_found[:descr]), "yesno") != "yes" do_destroy = false end end @args[:oata].destroy if do_destroy end def on_imiPreferences_activate @args[:oata].show_preferences end def on_imiWeekview_activate @args[:oata].show_worktime_overview end def on_window_destroy #Unconnect reload-event. Else it will crash on call to destroyed object. Also frees up various ressources. @args[:oata].ob.unconnect("object" => :Timelog, "conn_id" => @reload_id) @args[:oata].ob.unconnect("object" => :Timelog, "conn_id" => @reload_preparetransfer_id) @args[:oata].events.disconnect(:timelog_active_changed, @event_timelog_active_changed) if @event_timelog_active_changed @event_timelog_active_changed = nil Gtk.timeout_remove(@timeout_id) end def on_expOverview_activate(expander) if expander.expanded? @gui["window"].resize(@gui["window"].size[0], 480) self.timelog_info_trigger else Gtk.timeout_add(200) do self.timelog_info_trigger @gui["window"].resize(@gui["window"].size[0], 1) false end end end #This method handles the "Timelog info"-frame. Hides, shows and updates the info in it. def timelog_info_trigger if tlog = @args[:oata].timelog_active time_tracked = @args[:oata].timelog_active_time_tracked + tlog.time_total @gui["btnSwitch"].label = "#{_("Stop")} #{Knj::Strings.secs_to_human_short_time(time_tracked)}" end end def on_btnSwitch_clicked if @args[:oata].timelog_active @args[:oata].timelog_stop_tracking @gui["txtDescr"].grab_focus else descr = @gui["txtDescr"].text.to_s.strip task = @gui["cbTask"].sel if !task.is_a?(Knj::Datarow) task_id = 0 else task_id = task.id end timelog = @args[:oata].ob.get_by(:Timelog, { "task_id" => task_id, "descr_lower" => descr, "timestamp_day" => Time.now }) if !timelog timelog = @args[:oata].ob.add(:Timelog, { :task_id => task_id, :descr => descr }) end @args[:oata].timelog_active = timelog @gui["txtDescr"].text = "" @gui["cbTask"].sel = _("Choose:") end self.update_switch_button end #This method updates the switch button to start or stop, based on the if a timelog is tracked or not. def update_switch_button but = @gui["btnSwitch"] tlog_act = @args[:oata].timelog_active if tlog_act but.image = Gtk::Image.new(Gtk::Stock::MEDIA_STOP, Gtk::IconSize::BUTTON) @gui["txtDescr"].text = tlog_act[:descr] if task = tlog_act.task @gui["cbTask"].sel = task.name else @gui["cbTask"].sel = _("Choose:") end @gui["txtDescr"].sensitive = false @gui["cbTask"].sensitive = false else but.image = Gtk::Image.new(Gtk::Stock::MEDIA_RECORD, Gtk::IconSize::BUTTON) but.label = _("Start") @gui["txtDescr"].text = "" @gui["cbTask"].sel = _("Choose:") @gui["txtDescr"].sensitive = true @gui["cbTask"].sensitive = true end end #This method runs through all rows in the treeview and checks if a row should be marked with bold. It also increases the time in the time-column for the tracked timelog. def check_rows return nil if @tv_editting act_timelog = @args[:oata].timelog_active if act_timelog act_timelog_id = act_timelog.id else act_timelog_id = nil end rows_bold = [1, 2, 3, 4, 5, 6, 7, 11] @gui["tvTimelogs"].model.each do |model, path, iter| timelog_id = model.get_value(iter, 0).to_i bold = false iter_id = iter.to_s.to_i #Update time tracked. if timelog_id == act_timelog_id secs = act_timelog.time_total + @args[:oata].timelog_active_time_tracked iter[3] = "#{Knj::Strings.secs_to_human_time_str(secs, :secs => false)}" bold = true end #Set all columns to bold if not already set. if bold and !@bold_rows.key?(iter_id) rows_bold.each do |row_no| iter[row_no] = "#{model.get_value(iter, row_no)}" end @bold_rows[iter_id] = true end end end def on_miSyncStatic_activate @args[:oata].sync_static("transient_for" => @gui["window"]) end def on_btnMinus_clicked sel = @gui["tvTimelogs"].sel tlog = @args[:oata].ob.get(:Timelog, sel[0]) if sel if !sel or !tlog Knj::Gtk2.msgbox(_("Please choose a timelog to delete."), "warning") return nil end return nil if Knj::Gtk2.msgbox(_("Do you want to remove this timelog?"), "yesno") != "yes" begin @args[:oata].ob.delete(tlog) rescue => e Knj::Gtk2.msgbox(sprintf(_("Could not delete the timelog: %s"), e.message)) end end def on_btnPlus_clicked #Add new timelog to database. timelog = @args[:oata].ob.add(:Timelog) #Focus new timelog in treeview and open the in-line-editting for the description. added_id = timelog.id.to_i @gui["tvTimelogs"].model.each do |model, path, iter| timelog_id = model.get_value(iter, 0).to_i if timelog_id == added_id col = @gui["tvTimelogs"].columns[1] @gui["tvTimelogs"].set_cursor(path, col, true) break end end end #Redirects 'enter'-events to 'switch'-click-event. def on_txtDescr_activate(*args) self.on_btnSwitch_clicked end def on_btnSyncPrepareTransfer_clicked begin #Destroy this window and start syncing for real. @gui["window"].destroy @args[:oata].sync_real rescue => e Knj::Gtk2.msgbox(Knj::Errors.error_str(e)) end end end