qtimetrap 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +25 -0
- data/README.md +92 -0
- data/Rakefile +105 -0
- data/app/assets/icons/qtimetrap-icon-128.png +0 -0
- data/app/assets/icons/qtimetrap-icon-256.png +0 -0
- data/app/assets/icons/qtimetrap-icon-512.png +0 -0
- data/app/assets/icons/qtimetrap-icon.svg +123 -0
- data/app/components/entries/branch_hierarchy_helpers.rb +41 -0
- data/app/components/entries/leaf_archive_helpers.rb +23 -0
- data/app/components/entries/leaf_note_helpers.rb +118 -0
- data/app/components/entries/leaf_task_helpers.rb +81 -0
- data/app/components/entries/leaf_time_helpers.rb +105 -0
- data/app/components/entries/list_component.rb +118 -0
- data/app/components/entries/list_host_helpers.rb +27 -0
- data/app/components/entries/list_state_helpers.rb +59 -0
- data/app/components/entries/node_presentation_helpers.rb +29 -0
- data/app/components/entries/render_helpers.rb +25 -0
- data/app/components/entries/tree_helpers.rb +133 -0
- data/app/components/entries/tree_toolbar_helpers.rb +98 -0
- data/app/components/project_sidebar/archive_toggle_helpers.rb +22 -0
- data/app/components/project_sidebar/component.rb +125 -0
- data/app/components/project_sidebar/logo_helpers.rb +53 -0
- data/app/components/project_sidebar/project_button_helpers.rb +72 -0
- data/app/components/project_sidebar/project_selection_helpers.rb +94 -0
- data/app/components/project_sidebar/task_helpers.rb +94 -0
- data/app/components/project_sidebar/task_selection_helpers.rb +72 -0
- data/app/components/qt_ui_helpers.rb +27 -0
- data/app/components/tracker_controls/component.rb +80 -0
- data/app/components/tracker_controls/layout_builder.rb +100 -0
- data/app/components/tracker_controls/layout_helpers.rb +88 -0
- data/app/models/null_settings_store.rb +12 -0
- data/app/models/time_entry.rb +59 -0
- data/app/services/archived_entries_store.rb +71 -0
- data/app/services/formatters.rb +29 -0
- data/app/services/settings_store.rb +83 -0
- data/app/services/timetrap_gateway.rb +89 -0
- data/app/services/timetrap_gateway_logger.rb +54 -0
- data/app/services/timetrap_gateway_query_helpers.rb +102 -0
- data/app/services/timetrap_gateway_start_helpers.rb +28 -0
- data/app/services/timetrap_gateway_update_note_helpers.rb +48 -0
- data/app/services/timetrap_gateway_update_task_helpers.rb +47 -0
- data/app/services/timetrap_gateway_update_time_helpers.rb +50 -0
- data/app/styles/theme.rb +61 -0
- data/app/styles/themes/dark/application.qss +8 -0
- data/app/styles/themes/dark/entries_list.qss +235 -0
- data/app/styles/themes/dark/project_sidebar.qss +99 -0
- data/app/styles/themes/dark/snippets/app_background.qss +2 -0
- data/app/styles/themes/dark/snippets/button_ghost.qss +5 -0
- data/app/styles/themes/dark/snippets/button_start.qss +5 -0
- data/app/styles/themes/dark/snippets/button_stop.qss +5 -0
- data/app/styles/themes/dark/snippets/entries_host.qss +2 -0
- data/app/styles/themes/dark/snippets/entries_scroll.qss +16 -0
- data/app/styles/themes/dark/snippets/entry_row_day.qss +6 -0
- data/app/styles/themes/dark/snippets/entry_row_detail.qss +5 -0
- data/app/styles/themes/dark/snippets/entry_row_project.qss +6 -0
- data/app/styles/themes/dark/snippets/project_button.qss +5 -0
- data/app/styles/themes/dark/snippets/project_button_active.qss +5 -0
- data/app/styles/themes/dark/snippets/project_sidebar_heading.qss +2 -0
- data/app/styles/themes/dark/snippets/project_sidebar_logo.qss +3 -0
- data/app/styles/themes/dark/snippets/project_sidebar_panel.qss +2 -0
- data/app/styles/themes/dark/snippets/tracker_clock.qss +2 -0
- data/app/styles/themes/dark/snippets/tracker_input.qss +5 -0
- data/app/styles/themes/dark/snippets/tracker_project_label.qss +5 -0
- data/app/styles/themes/dark/snippets/tracker_row.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_summary.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_timer.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_title.qss +3 -0
- data/app/styles/themes/dark/snippets/tracker_topbar.qss +2 -0
- data/app/styles/themes/dark/tracker_controls.qss +84 -0
- data/app/styles/themes/light/application.qss +8 -0
- data/app/styles/themes/light/entries_list.qss +235 -0
- data/app/styles/themes/light/project_sidebar.qss +99 -0
- data/app/styles/themes/light/snippets/app_background.qss +2 -0
- data/app/styles/themes/light/snippets/button_ghost.qss +5 -0
- data/app/styles/themes/light/snippets/button_start.qss +5 -0
- data/app/styles/themes/light/snippets/button_stop.qss +5 -0
- data/app/styles/themes/light/snippets/entries_host.qss +2 -0
- data/app/styles/themes/light/snippets/entries_scroll.qss +16 -0
- data/app/styles/themes/light/snippets/entry_row_day.qss +6 -0
- data/app/styles/themes/light/snippets/entry_row_detail.qss +5 -0
- data/app/styles/themes/light/snippets/entry_row_project.qss +6 -0
- data/app/styles/themes/light/snippets/project_button.qss +5 -0
- data/app/styles/themes/light/snippets/project_button_active.qss +5 -0
- data/app/styles/themes/light/snippets/project_sidebar_heading.qss +2 -0
- data/app/styles/themes/light/snippets/project_sidebar_logo.qss +3 -0
- data/app/styles/themes/light/snippets/project_sidebar_panel.qss +2 -0
- data/app/styles/themes/light/snippets/tracker_clock.qss +2 -0
- data/app/styles/themes/light/snippets/tracker_input.qss +5 -0
- data/app/styles/themes/light/snippets/tracker_project_label.qss +5 -0
- data/app/styles/themes/light/snippets/tracker_row.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_summary.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_timer.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_title.qss +3 -0
- data/app/styles/themes/light/snippets/tracker_topbar.qss +2 -0
- data/app/styles/themes/light/tracker_controls.qss +84 -0
- data/app/view_models/entry_nodes_builder.rb +109 -0
- data/app/view_models/main_view_model.rb +135 -0
- data/app/view_models/main_view_model_archive_mode_helpers.rb +102 -0
- data/app/view_models/main_view_model_entry_note_helpers.rb +30 -0
- data/app/view_models/main_view_model_entry_task_helpers.rb +39 -0
- data/app/view_models/main_view_model_entry_time_helpers.rb +45 -0
- data/app/view_models/main_view_model_sheet_helpers.rb +74 -0
- data/app/view_models/main_view_model_task_filter_helpers.rb +58 -0
- data/app/view_models/main_view_model_time_range_filter_helpers.rb +23 -0
- data/app/views/main_window.rb +126 -0
- data/app/views/main_window_layout_builder.rb +114 -0
- data/app/views/main_window_runtime.rb +123 -0
- data/app/views/main_window_runtime_archive_helpers.rb +26 -0
- data/app/views/main_window_runtime_entry_task_helpers.rb +21 -0
- data/app/views/main_window_runtime_entry_time_helpers.rb +17 -0
- data/app/views/main_window_runtime_key_helpers.rb +55 -0
- data/app/views/main_window_runtime_render_helpers.rb +50 -0
- data/app/views/main_window_splitter_toggle_bootstrap_helpers.rb +34 -0
- data/app/views/main_window_splitter_toggle_helpers.rb +103 -0
- data/app/views/main_window_splitter_toggle_hover_helpers.rb +71 -0
- data/app/views/main_window_splitter_toggle_layout_helpers.rb +24 -0
- data/app/views/main_window_ui_helpers.rb +45 -0
- data/app/views/window_icon_loader.rb +37 -0
- data/bin/qtimetrap +7 -0
- data/config/application.rb +87 -0
- data/config/environments/development.rb +5 -0
- data/config/environments/production.rb +5 -0
- data/config/initializers/theme.rb +5 -0
- data/config/initializers/timetrap.rb +5 -0
- data/lib/qtimetrap/configuration.rb +15 -0
- data/lib/qtimetrap/container.rb +57 -0
- data/lib/qtimetrap/version.rb +5 -0
- data/lib/qtimetrap.rb +25 -0
- metadata +207 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Entry-leaf helpers for editable start/end time controls.
|
|
6
|
+
module LeafTimeHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def add_entry_time_widgets(row_layout, row, node)
|
|
10
|
+
start_input = build_entry_time_input(
|
|
11
|
+
row,
|
|
12
|
+
object_name: 'entry_node_entry_start',
|
|
13
|
+
value: node.fetch(:start_label, '--:--'),
|
|
14
|
+
placeholder: '--:--'
|
|
15
|
+
)
|
|
16
|
+
end_input = build_entry_time_input(
|
|
17
|
+
row,
|
|
18
|
+
object_name: 'entry_node_entry_end',
|
|
19
|
+
value: node.fetch(:end_label, 'running'),
|
|
20
|
+
placeholder: 'running'
|
|
21
|
+
)
|
|
22
|
+
row_layout.add_widget(build_entry_time_group(row, start_input, end_input))
|
|
23
|
+
[start_input, end_input]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_entry_time_group(row, start_input, end_input)
|
|
27
|
+
QWidget.new(row).tap do |time_group|
|
|
28
|
+
time_group.set_object_name('entry_node_entry_time_group')
|
|
29
|
+
QHBoxLayout.new(time_group).tap do |time_layout|
|
|
30
|
+
time_layout.set_contents_margins(0, 0, 0, 0)
|
|
31
|
+
time_layout.set_spacing(4)
|
|
32
|
+
time_layout.add_widget(start_input)
|
|
33
|
+
time_layout.add_widget(build_entry_time_separator(time_group))
|
|
34
|
+
time_layout.add_widget(end_input)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def build_entry_time_separator(row)
|
|
40
|
+
QLabel.new(row).tap do |separator|
|
|
41
|
+
separator.set_object_name('entry_node_entry_time_sep')
|
|
42
|
+
separator.set_text('-')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def build_entry_time_input(row, object_name:, value:, placeholder:)
|
|
47
|
+
QLineEdit.new(row).tap do |time_input|
|
|
48
|
+
time_input.set_object_name(object_name)
|
|
49
|
+
time_input.text = value.to_s
|
|
50
|
+
time_input.set_placeholder_text(placeholder)
|
|
51
|
+
time_input.set_alignment(Qt::AlignCenter)
|
|
52
|
+
time_input.set_read_only(true)
|
|
53
|
+
time_input.set_fixed_width(58)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bind_entry_time_input_events(start_input, end_input, entry_id)
|
|
58
|
+
bind_single_time_input(start_input, entry_id, start_input, end_input)
|
|
59
|
+
bind_single_time_input(end_input, entry_id, start_input, end_input)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def bind_single_time_input(time_input, entry_id, start_input, end_input)
|
|
63
|
+
time_input.on(:mouse_button_press) { |_| activate_entry_note_input(time_input) }
|
|
64
|
+
time_input.on(:key_press) do |event|
|
|
65
|
+
handle_entry_time_key_press(
|
|
66
|
+
time_input: time_input,
|
|
67
|
+
entry_id: entry_id,
|
|
68
|
+
start_input: start_input,
|
|
69
|
+
end_input: end_input,
|
|
70
|
+
event: event
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
time_input.connect('returnPressed') do |_|
|
|
74
|
+
handle_entry_time_commit(
|
|
75
|
+
time_input: time_input,
|
|
76
|
+
entry_id: entry_id,
|
|
77
|
+
start_input: start_input,
|
|
78
|
+
end_input: end_input
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
time_input.on(:focus_out) { |_| handle_entry_note_focus_out(time_input) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def handle_entry_time_key_press(time_input:, entry_id:, start_input:, end_input:, event:)
|
|
85
|
+
return unless enter_key?(event)
|
|
86
|
+
|
|
87
|
+
handle_entry_time_commit(
|
|
88
|
+
time_input: time_input,
|
|
89
|
+
entry_id: entry_id,
|
|
90
|
+
start_input: start_input,
|
|
91
|
+
end_input: end_input
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_entry_time_commit(time_input:, entry_id:, start_input:, end_input:)
|
|
96
|
+
return if time_input.is_read_only
|
|
97
|
+
|
|
98
|
+
deactivate_entry_note_input(time_input)
|
|
99
|
+
return unless on_entry_time_change
|
|
100
|
+
|
|
101
|
+
on_entry_time_change.call(entry_id, start_input.text.to_s, end_input.text.to_s)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Renders expandable week/day/project nodes and leaf time entries.
|
|
6
|
+
class ListComponent
|
|
7
|
+
include ListHostHelpers
|
|
8
|
+
include ListStateHelpers
|
|
9
|
+
include QtUiHelpers
|
|
10
|
+
include TreeHelpers
|
|
11
|
+
include RenderHelpers
|
|
12
|
+
|
|
13
|
+
HOST_HORIZONTAL_MARGINS = 28
|
|
14
|
+
WIDTH_PADDING = 24
|
|
15
|
+
TIME_FILTER_DEBOUNCE_MS = 220
|
|
16
|
+
|
|
17
|
+
attr_reader :widget
|
|
18
|
+
|
|
19
|
+
def initialize(parent:, callbacks: {}, task_suggestions_for_project: nil)
|
|
20
|
+
@parent = parent
|
|
21
|
+
@on_entry_note_change = callbacks[:on_entry_note_change]
|
|
22
|
+
@on_entry_task_change = callbacks[:on_entry_task_change]
|
|
23
|
+
@task_suggestions_for_project = task_suggestions_for_project
|
|
24
|
+
@on_entry_time_change = callbacks[:on_entry_time_change]
|
|
25
|
+
@on_entry_archive = callbacks[:on_entry_archive]
|
|
26
|
+
@on_time_range_change = callbacks[:on_time_range_change]
|
|
27
|
+
initialize_state!
|
|
28
|
+
build
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render(nodes)
|
|
32
|
+
return if rendering
|
|
33
|
+
|
|
34
|
+
@rendering = true
|
|
35
|
+
@current_nodes = Array(nodes)
|
|
36
|
+
@branch_bindings = {}
|
|
37
|
+
@leaf_labels = []
|
|
38
|
+
@entry_rows = []
|
|
39
|
+
with_widget_updates_suspended { render_contents }
|
|
40
|
+
ensure
|
|
41
|
+
@rendering = false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update_time_range_inputs(from_at:, to_at:)
|
|
45
|
+
@syncing_time_filters = true
|
|
46
|
+
set_time_filter_state(
|
|
47
|
+
toggle: time_filter_from_toggle,
|
|
48
|
+
input: time_filter_from_input,
|
|
49
|
+
value: from_at
|
|
50
|
+
)
|
|
51
|
+
set_time_filter_state(
|
|
52
|
+
toggle: time_filter_to_toggle,
|
|
53
|
+
input: time_filter_to_input,
|
|
54
|
+
value: to_at
|
|
55
|
+
)
|
|
56
|
+
ensure
|
|
57
|
+
@syncing_time_filters = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def shutdown
|
|
61
|
+
return unless time_filter_debounce
|
|
62
|
+
|
|
63
|
+
time_filter_debounce.stop if time_filter_debounce.is_active
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
attr_reader :parent, :host, :host_layout, :expanded, :current_nodes, :branch_bindings, :leaf_labels, :entry_rows,
|
|
69
|
+
:rendering, :scroll_area, :on_entry_note_change, :on_entry_task_change, :task_suggestions_for_project,
|
|
70
|
+
:on_entry_time_change, :on_entry_archive, :on_time_range_change,
|
|
71
|
+
:time_filter_from_toggle, :time_filter_to_toggle, :time_filter_from_input, :time_filter_to_input,
|
|
72
|
+
:time_filter_debounce
|
|
73
|
+
|
|
74
|
+
def build
|
|
75
|
+
@widget = QWidget.new(parent)
|
|
76
|
+
widget.set_object_name('entries_panel')
|
|
77
|
+
panel_layout = build_panel_layout
|
|
78
|
+
@time_filter_debounce = build_time_filter_debounce_timer
|
|
79
|
+
panel_layout.add_widget(build_toolbar(parent_widget: widget))
|
|
80
|
+
@scroll_area = build_scroll_area
|
|
81
|
+
bind_scroll_resize
|
|
82
|
+
panel_layout.add_widget(scroll_area)
|
|
83
|
+
panel_layout.set_stretch(1, 1)
|
|
84
|
+
rebuild_host!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def branch_button_width
|
|
88
|
+
available = scroll_area.width - HOST_HORIZONTAL_MARGINS - WIDTH_PADDING
|
|
89
|
+
[available, 120].max
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def adjust_node_widths
|
|
93
|
+
width = branch_button_width
|
|
94
|
+
branch_bindings.each_value { |binding| binding.fetch(:button).set_fixed_width(width) }
|
|
95
|
+
leaf_labels.each { |label| label.set_fixed_width(width) }
|
|
96
|
+
entry_rows.each { |row| row.set_fixed_width(width) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_panel_layout
|
|
100
|
+
QVBoxLayout.new(widget).tap do |layout|
|
|
101
|
+
layout.set_contents_margins(0, 0, 0, 0)
|
|
102
|
+
layout.set_spacing(6)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_scroll_area
|
|
107
|
+
QScrollArea.new(widget).tap do |area|
|
|
108
|
+
area.set_object_name('entries_scroll')
|
|
109
|
+
area.set_widget_resizable(true)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def bind_scroll_resize
|
|
114
|
+
scroll_area.on(:resize) { |_| adjust_node_widths }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Host/viewport helpers for entries list rendering.
|
|
6
|
+
module ListHostHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_host
|
|
10
|
+
QWidget.new(scroll_area).tap { |container| container.set_object_name('entries_host') }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def build_host_layout
|
|
14
|
+
QVBoxLayout.new(host).tap do |layout|
|
|
15
|
+
layout.set_contents_margins(14, 10, 14, 10)
|
|
16
|
+
layout.set_spacing(2)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def rebuild_host!
|
|
21
|
+
@host = build_host
|
|
22
|
+
@host_layout = build_host_layout
|
|
23
|
+
scroll_area.set_widget(host)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Internal state and debounce helpers for entries list component.
|
|
6
|
+
module ListStateHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def initialize_state!
|
|
10
|
+
initialize_tree_state!
|
|
11
|
+
initialize_time_filter_state!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize_tree_state!
|
|
15
|
+
@expanded = {}
|
|
16
|
+
@current_nodes = []
|
|
17
|
+
@branch_bindings = {}
|
|
18
|
+
@leaf_labels = []
|
|
19
|
+
@entry_rows = []
|
|
20
|
+
@rendering = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize_time_filter_state!
|
|
24
|
+
@time_filter_from_toggle = nil
|
|
25
|
+
@time_filter_to_toggle = nil
|
|
26
|
+
@time_filter_from_input = nil
|
|
27
|
+
@time_filter_to_input = nil
|
|
28
|
+
@time_filter_debounce = nil
|
|
29
|
+
@syncing_time_filters = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def set_time_filter_state(toggle:, input:, value:)
|
|
33
|
+
enabled = !value.nil?
|
|
34
|
+
toggle.set_checked(enabled)
|
|
35
|
+
input.set_date_time(value) if enabled
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def syncing_time_filters?
|
|
39
|
+
@syncing_time_filters
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def schedule_time_range_filter_changed
|
|
43
|
+
return if syncing_time_filters?
|
|
44
|
+
return unless on_time_range_change
|
|
45
|
+
|
|
46
|
+
time_filter_debounce.stop if time_filter_debounce.is_active
|
|
47
|
+
time_filter_debounce.start
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_time_filter_debounce_timer
|
|
51
|
+
QTimer.new(parent).tap do |timer|
|
|
52
|
+
timer.set_single_shot(true)
|
|
53
|
+
timer.set_interval(self.class::TIME_FILTER_DEBOUNCE_MS)
|
|
54
|
+
timer.connect('timeout') { |_| emit_time_range_filter_changed }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Node text/object-name helpers for entries tree rendering.
|
|
6
|
+
module NodePresentationHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def object_name_for(node)
|
|
10
|
+
case node.fetch(:type)
|
|
11
|
+
when :week then 'entry_node_week'
|
|
12
|
+
when :day then 'entry_node_day'
|
|
13
|
+
when :project then 'entry_node_project'
|
|
14
|
+
when :entry then 'entry_node_entry'
|
|
15
|
+
else 'entry_node_empty'
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def indent(level)
|
|
20
|
+
' ' * level
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def branch_button_text(level, label, expanded_state)
|
|
24
|
+
prefix = expanded_state ? '▾' : '▸'
|
|
25
|
+
"#{indent(level)}#{prefix} #{label}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Render-cycle helpers to reduce flicker during entries tree rebuild.
|
|
6
|
+
module RenderHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def with_widget_updates_suspended
|
|
10
|
+
widget.set_updates_enabled(false)
|
|
11
|
+
yield
|
|
12
|
+
ensure
|
|
13
|
+
widget.set_updates_enabled(true)
|
|
14
|
+
widget.update
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render_contents
|
|
18
|
+
rebuild_host!
|
|
19
|
+
render_nodes(current_nodes, 0)
|
|
20
|
+
adjust_node_widths
|
|
21
|
+
host_layout.add_stretch(1)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Helper methods for entries tree rendering and expand/collapse controls.
|
|
6
|
+
module TreeHelpers
|
|
7
|
+
include BranchHierarchyHelpers
|
|
8
|
+
include LeafArchiveHelpers
|
|
9
|
+
include LeafNoteHelpers
|
|
10
|
+
include LeafTaskHelpers
|
|
11
|
+
include LeafTimeHelpers
|
|
12
|
+
include NodePresentationHelpers
|
|
13
|
+
include TreeToolbarHelpers
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def on_filter_toggle_changed
|
|
18
|
+
schedule_time_range_filter_changed
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_initial_filter_ui_state
|
|
22
|
+
time_filter_from_toggle.set_checked(false)
|
|
23
|
+
time_filter_to_toggle.set_checked(false)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def emit_time_range_filter_changed
|
|
27
|
+
return if syncing_time_filters?
|
|
28
|
+
return unless on_time_range_change
|
|
29
|
+
|
|
30
|
+
on_time_range_change.call(selected_time_filter(time_filter_from_toggle, time_filter_from_input),
|
|
31
|
+
selected_time_filter(time_filter_to_toggle, time_filter_to_input))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def selected_time_filter(toggle, input)
|
|
35
|
+
return nil unless filter_toggle_checked?(toggle)
|
|
36
|
+
|
|
37
|
+
input.date_time
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def filter_toggle_checked?(toggle)
|
|
41
|
+
value = toggle.is_checked
|
|
42
|
+
[true, 1].include?(value)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expand_all!
|
|
46
|
+
set_all_branch_nodes(current_nodes, true)
|
|
47
|
+
apply_all_branch_states
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collapse_all!
|
|
51
|
+
set_all_branch_nodes(current_nodes, false)
|
|
52
|
+
apply_all_branch_states
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def set_all_branch_nodes(nodes, value)
|
|
56
|
+
nodes.each do |node|
|
|
57
|
+
next unless branch_node?(node)
|
|
58
|
+
|
|
59
|
+
expanded[node.fetch(:id)] = value
|
|
60
|
+
set_all_branch_nodes(node.fetch(:children), value)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_nodes(nodes, level, layout: host_layout, parent_widget: host)
|
|
65
|
+
nodes.each { |node| render_node(node, level, layout: layout, parent_widget: parent_widget) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def render_node(node, level, layout:, parent_widget:)
|
|
69
|
+
if branch_node?(node)
|
|
70
|
+
render_branch_node(node, level, layout: layout, parent_widget: parent_widget)
|
|
71
|
+
else
|
|
72
|
+
render_leaf_node(node, level, layout: layout, parent_widget: parent_widget)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_branch_node(node, level, layout:, parent_widget:)
|
|
77
|
+
node_id = node.fetch(:id)
|
|
78
|
+
label = node.fetch(:label)
|
|
79
|
+
expanded_state = expanded.fetch(node.fetch(:id), true)
|
|
80
|
+
button = build_branch_button(node, level, expanded_state, parent_widget: parent_widget)
|
|
81
|
+
layout.add_widget(button)
|
|
82
|
+
children_container, children_layout = build_children_container(parent_widget)
|
|
83
|
+
layout.add_widget(children_container)
|
|
84
|
+
register_branch_binding(
|
|
85
|
+
node_id: node_id,
|
|
86
|
+
button: button,
|
|
87
|
+
children_container: children_container,
|
|
88
|
+
label: label,
|
|
89
|
+
level: level
|
|
90
|
+
)
|
|
91
|
+
render_nodes(node.fetch(:children), level + 1, layout: children_layout, parent_widget: children_container)
|
|
92
|
+
apply_branch_state(node_id, expanded_state)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def render_leaf_node(node, level, layout:, parent_widget:)
|
|
96
|
+
if node.fetch(:type) == :entry
|
|
97
|
+
render_entry_leaf_node(node, level, layout: layout, parent_widget: parent_widget)
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
render_default_leaf_node(node, level, parent_widget: parent_widget, layout: layout)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def render_default_leaf_node(node, level, parent_widget:, layout:)
|
|
105
|
+
label = QLabel.new(parent_widget)
|
|
106
|
+
label.set_object_name(object_name_for(node))
|
|
107
|
+
label.set_text("#{indent(level)}#{node.fetch(:label)}")
|
|
108
|
+
label.set_fixed_width(branch_button_width)
|
|
109
|
+
label.set_fixed_height(32)
|
|
110
|
+
leaf_labels << label
|
|
111
|
+
layout.add_widget(label)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def toggle_node(node_id)
|
|
115
|
+
expanded[node_id] = !expanded.fetch(node_id, true)
|
|
116
|
+
apply_branch_state(node_id, expanded[node_id])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def branch_node?(node)
|
|
120
|
+
!node.fetch(:children).empty?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_branch_button(node, level, expanded_state, parent_widget:)
|
|
124
|
+
text = branch_button_text(level, node.fetch(:label), expanded_state)
|
|
125
|
+
button = build_button(parent_widget, object_name_for(node), text, 0, 32)
|
|
126
|
+
button.set_fixed_width(branch_button_width)
|
|
127
|
+
node_id = node.fetch(:id)
|
|
128
|
+
button.connect('clicked') { |_| toggle_node(node_id) }
|
|
129
|
+
button
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Entries
|
|
5
|
+
# Toolbar builders for entries tree controls and time-range filters.
|
|
6
|
+
module TreeToolbarHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_toolbar(parent_widget:)
|
|
10
|
+
toolbar = QWidget.new(parent_widget)
|
|
11
|
+
toolbar.set_object_name('entries_toolbar')
|
|
12
|
+
layout = build_toolbar_layout(toolbar)
|
|
13
|
+
add_tree_toolbar_buttons(layout, toolbar)
|
|
14
|
+
add_time_filter_controls(layout, toolbar)
|
|
15
|
+
set_initial_filter_ui_state
|
|
16
|
+
layout.add_stretch(1)
|
|
17
|
+
toolbar
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_toolbar_layout(toolbar)
|
|
21
|
+
QHBoxLayout.new(toolbar).tap do |layout|
|
|
22
|
+
layout.set_contents_margins(0, 0, 0, 0)
|
|
23
|
+
layout.set_spacing(8)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_tree_toolbar_buttons(layout, toolbar)
|
|
28
|
+
add_toolbar_button(layout, toolbar, 'entries_expand_all', 'EXPAND ALL') { expand_all! }
|
|
29
|
+
add_toolbar_button(layout, toolbar, 'entries_collapse_all', 'COLLAPSE ALL') { collapse_all! }
|
|
30
|
+
layout.add_spacing(8)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_toolbar_button(layout, toolbar, name, text, &)
|
|
34
|
+
layout.add_widget(build_toolbar_button(toolbar, name, text, &))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_time_filter_controls(layout, toolbar)
|
|
38
|
+
@time_filter_from_toggle = add_filter_control(
|
|
39
|
+
layout: layout,
|
|
40
|
+
toolbar: toolbar,
|
|
41
|
+
toggle_name: 'entries_time_filter_from_toggle',
|
|
42
|
+
toggle_text: 'FROM',
|
|
43
|
+
input_name: 'entries_time_filter_from'
|
|
44
|
+
)
|
|
45
|
+
@time_filter_to_toggle = add_filter_control(
|
|
46
|
+
layout: layout,
|
|
47
|
+
toolbar: toolbar,
|
|
48
|
+
toggle_name: 'entries_time_filter_to_toggle',
|
|
49
|
+
toggle_text: 'TO',
|
|
50
|
+
input_name: 'entries_time_filter_to'
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_filter_control(layout:, toolbar:, toggle_name:, toggle_text:, input_name:)
|
|
55
|
+
toggle = build_filter_toggle(toolbar, toggle_name, toggle_text)
|
|
56
|
+
layout.add_widget(toggle)
|
|
57
|
+
input = build_filter_input(toolbar, input_name)
|
|
58
|
+
layout.add_widget(input)
|
|
59
|
+
assign_filter_input(toggle_name, input)
|
|
60
|
+
toggle
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def assign_filter_input(toggle_name, input)
|
|
64
|
+
if toggle_name == 'entries_time_filter_from_toggle'
|
|
65
|
+
@time_filter_from_input = input
|
|
66
|
+
else
|
|
67
|
+
@time_filter_to_input = input
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_toolbar_button(parent_widget, name, text)
|
|
72
|
+
build_button(parent_widget, name, text, 136, 28).tap { |button| button.connect('clicked') { |_| yield } }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def build_filter_toggle(parent_widget, name, text)
|
|
76
|
+
QCheckBox.new(parent_widget).tap do |checkbox|
|
|
77
|
+
checkbox.set_object_name(name)
|
|
78
|
+
checkbox.set_text(text)
|
|
79
|
+
checkbox.set_focus_policy(Qt::NoFocus)
|
|
80
|
+
checkbox.set_fixed_height(28)
|
|
81
|
+
checkbox.connect('clicked') { |_| on_filter_toggle_changed }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_filter_input(parent_widget, name)
|
|
86
|
+
QDateTimeEdit.new(parent_widget).tap do |input|
|
|
87
|
+
input.set_object_name(name)
|
|
88
|
+
input.set_focus_policy(Qt::ClickFocus)
|
|
89
|
+
input.set_fixed_width(172)
|
|
90
|
+
input.set_calendar_popup(true)
|
|
91
|
+
input.set_display_format('yyyy-MM-dd HH:mm')
|
|
92
|
+
input.set_date_time(Time.now)
|
|
93
|
+
input.connect('dateTimeChanged(QDateTime)') { |_| schedule_time_range_filter_changed }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module ProjectSidebar
|
|
5
|
+
# Bottom archive mode toggle button in sidebar.
|
|
6
|
+
module ArchiveToggleHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def build_archive_toggle_button
|
|
10
|
+
QPushButton.new(widget).tap do |button|
|
|
11
|
+
button.set_object_name('sidebar_archive_toggle')
|
|
12
|
+
button.set_checkable(true)
|
|
13
|
+
button.set_focus_policy(Qt::NoFocus)
|
|
14
|
+
button.set_fixed_height(30)
|
|
15
|
+
button.set_text("\u{1F5C3}")
|
|
16
|
+
button.set_tool_tip('Show archived entries only')
|
|
17
|
+
button.connect('clicked') { |_| on_archive_mode_toggled&.call(button.is_checked) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|