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,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Main Qt window wiring together components and user interactions.
|
|
6
|
+
class MainWindow
|
|
7
|
+
include MainWindowRuntime
|
|
8
|
+
include MainWindowUiHelpers
|
|
9
|
+
|
|
10
|
+
WINDOW_W = 1380
|
|
11
|
+
WINDOW_H = 860
|
|
12
|
+
THEMES = %w[light dark].freeze
|
|
13
|
+
HEARTBEAT_MS = 33
|
|
14
|
+
|
|
15
|
+
def initialize(
|
|
16
|
+
view_model: ViewModels::MainViewModel.new,
|
|
17
|
+
theme: Styles::Theme.new(name: 'light', root: Application.root),
|
|
18
|
+
settings_store: Models::NullSettingsStore.new
|
|
19
|
+
)
|
|
20
|
+
@view_model = view_model
|
|
21
|
+
@theme = theme
|
|
22
|
+
@settings_store = settings_store
|
|
23
|
+
@pending_refresh = true
|
|
24
|
+
@shutdown_requested = false
|
|
25
|
+
|
|
26
|
+
build_window
|
|
27
|
+
connect_key_events
|
|
28
|
+
connect_shortcuts
|
|
29
|
+
connect_heartbeat
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def show = window.show
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
persist_window_geometry
|
|
36
|
+
window.close
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def request_shutdown = @shutdown_requested = true
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
attr_reader :view_model, :window, :theme, :settings_store, :sidebar, :controls, :entries, :heartbeat
|
|
44
|
+
|
|
45
|
+
def build_window
|
|
46
|
+
@window = build_base_window
|
|
47
|
+
restore_window_geometry
|
|
48
|
+
set_window_icon
|
|
49
|
+
window.set_style_sheet(theme.application_stylesheet)
|
|
50
|
+
ui = MainWindowLayoutBuilder.new(
|
|
51
|
+
window: window,
|
|
52
|
+
callbacks: layout_callbacks
|
|
53
|
+
).build
|
|
54
|
+
@sidebar = ui.fetch(:sidebar)
|
|
55
|
+
@controls = ui.fetch(:controls)
|
|
56
|
+
@entries = ui.fetch(:entries)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def connect_heartbeat
|
|
60
|
+
@heartbeat = QTimer.new(window)
|
|
61
|
+
heartbeat.set_interval(HEARTBEAT_MS)
|
|
62
|
+
heartbeat.connect('timeout') { |_| on_tick }
|
|
63
|
+
heartbeat.start
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def connect_key_events
|
|
67
|
+
window.on(:key_press) { |event| on_key_press(event) }
|
|
68
|
+
register_blur_click_source(window)
|
|
69
|
+
register_blur_click_source(sidebar.widget)
|
|
70
|
+
register_blur_click_source(controls.widget)
|
|
71
|
+
register_blur_click_source(entries.widget)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def connect_shortcuts
|
|
75
|
+
@space_shortcut = QShortcut.new(QKeySequence.new('Space'), window)
|
|
76
|
+
@space_shortcut.connect('activated') { |_| on_space_shortcut }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def register_blur_click_source(widget)
|
|
80
|
+
widget.on(:mouse_button_press) { |event| on_mouse_button_press(event, source_widget: widget) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def restore_window_geometry
|
|
84
|
+
geometry = settings_store.read_window_geometry
|
|
85
|
+
return unless geometry
|
|
86
|
+
|
|
87
|
+
window.set_geometry(
|
|
88
|
+
geometry.fetch(:left),
|
|
89
|
+
geometry.fetch(:top),
|
|
90
|
+
geometry.fetch(:width),
|
|
91
|
+
geometry.fetch(:height)
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def persist_window_geometry
|
|
96
|
+
settings_store.write_window_geometry(
|
|
97
|
+
left: window.x,
|
|
98
|
+
top: window.y,
|
|
99
|
+
width: window.width,
|
|
100
|
+
height: window.height
|
|
101
|
+
)
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
warn("[qtimetrap] save geometry failed: #{e.class}: #{e.message}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def layout_callbacks
|
|
107
|
+
{
|
|
108
|
+
on_project_selected: method(:handle_project_selected),
|
|
109
|
+
on_task_selected: method(:handle_task_selected),
|
|
110
|
+
on_archive_mode_toggled: method(:handle_archive_mode_toggled),
|
|
111
|
+
on_start: method(:handle_start),
|
|
112
|
+
on_project_change: method(:handle_project_input),
|
|
113
|
+
on_time_range_change: method(:handle_time_range_changed),
|
|
114
|
+
on_entry_note_change: method(:handle_entry_note_changed),
|
|
115
|
+
on_entry_task_change: method(:handle_entry_task_changed),
|
|
116
|
+
on_entry_time_change: method(:handle_entry_time_changed),
|
|
117
|
+
task_suggestions_for_project: method(:task_suggestions_for_project),
|
|
118
|
+
on_entry_archive: method(:handle_entry_archived),
|
|
119
|
+
on_stop: method(:handle_stop),
|
|
120
|
+
on_refresh: -> { @pending_refresh = true },
|
|
121
|
+
on_switch_theme: method(:switch_theme!)
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Builds main window layout and returns initialized component instances.
|
|
6
|
+
class MainWindowLayoutBuilder
|
|
7
|
+
include MainWindowSplitterToggleHelpers
|
|
8
|
+
|
|
9
|
+
SIDEBAR_WIDTH = 220
|
|
10
|
+
SIDEBAR_MIN_WIDTH = 180
|
|
11
|
+
SIDEBAR_MAX_WIDTH = 520
|
|
12
|
+
|
|
13
|
+
def initialize(window:, callbacks:)
|
|
14
|
+
@window = window
|
|
15
|
+
@callbacks = callbacks
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build
|
|
19
|
+
root = QHBoxLayout.new(window)
|
|
20
|
+
root.set_contents_margins(0, 0, 0, 0)
|
|
21
|
+
root.set_spacing(0)
|
|
22
|
+
splitter, sidebar, controls, entries = build_splitter_and_content
|
|
23
|
+
root.add_widget(splitter)
|
|
24
|
+
{ sidebar: sidebar, controls: controls, entries: entries }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :window, :callbacks
|
|
30
|
+
|
|
31
|
+
def build_splitter_and_content
|
|
32
|
+
splitter = QSplitter.new(window)
|
|
33
|
+
splitter.set_orientation(Qt::Horizontal)
|
|
34
|
+
splitter.set_mouse_tracking(true)
|
|
35
|
+
sidebar = build_sidebar(parent: splitter)
|
|
36
|
+
content, controls, entries = build_content(parent: splitter)
|
|
37
|
+
splitter.add_widget(sidebar.widget)
|
|
38
|
+
splitter.add_widget(content)
|
|
39
|
+
configure_splitter(splitter)
|
|
40
|
+
add_sidebar_toggle_button(window: window, splitter: splitter, sidebar_widget: sidebar.widget)
|
|
41
|
+
[splitter, sidebar, controls, entries]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def configure_splitter(splitter)
|
|
45
|
+
splitter.set_stretch_factor(0, 0)
|
|
46
|
+
splitter.set_stretch_factor(1, 1)
|
|
47
|
+
splitter.set_collapsible(0, false)
|
|
48
|
+
splitter.set_collapsible(1, false)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_sidebar(parent:)
|
|
52
|
+
QTimetrap::ProjectSidebar::Component.new(
|
|
53
|
+
parent: parent,
|
|
54
|
+
on_project_selected: callbacks.fetch(:on_project_selected),
|
|
55
|
+
on_task_selected: callbacks.fetch(:on_task_selected),
|
|
56
|
+
on_archive_mode_toggled: callbacks.fetch(:on_archive_mode_toggled)
|
|
57
|
+
).tap do |component|
|
|
58
|
+
component.widget.set_base_size(SIDEBAR_WIDTH, 0)
|
|
59
|
+
component.widget.set_minimum_width(SIDEBAR_MIN_WIDTH)
|
|
60
|
+
component.widget.set_maximum_width(SIDEBAR_MAX_WIDTH)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_content(parent:)
|
|
65
|
+
content, layout = build_content_widget(parent: parent)
|
|
66
|
+
controls = build_controls(content)
|
|
67
|
+
entries = build_entries_component(content)
|
|
68
|
+
layout.add_widget(controls.widget)
|
|
69
|
+
layout.add_widget(entries.widget)
|
|
70
|
+
layout.set_stretch(1, 1)
|
|
71
|
+
[content, controls, entries]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_entries_component(parent)
|
|
75
|
+
QTimetrap::Entries::ListComponent.new(
|
|
76
|
+
parent: parent,
|
|
77
|
+
callbacks: entries_callbacks,
|
|
78
|
+
task_suggestions_for_project: callbacks.fetch(:task_suggestions_for_project)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def entries_callbacks
|
|
83
|
+
{
|
|
84
|
+
on_entry_note_change: callbacks.fetch(:on_entry_note_change),
|
|
85
|
+
on_entry_task_change: callbacks.fetch(:on_entry_task_change),
|
|
86
|
+
on_entry_time_change: callbacks.fetch(:on_entry_time_change),
|
|
87
|
+
on_entry_archive: callbacks.fetch(:on_entry_archive),
|
|
88
|
+
on_time_range_change: callbacks.fetch(:on_time_range_change)
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_content_widget(parent:)
|
|
93
|
+
content = QWidget.new(parent)
|
|
94
|
+
layout = QVBoxLayout.new(content)
|
|
95
|
+
layout.set_contents_margins(14, 8, 14, 8)
|
|
96
|
+
layout.set_spacing(10)
|
|
97
|
+
[content, layout]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_controls(content)
|
|
101
|
+
QTimetrap::TrackerControls::Component.new(
|
|
102
|
+
parent: content,
|
|
103
|
+
callbacks: {
|
|
104
|
+
on_start: callbacks.fetch(:on_start),
|
|
105
|
+
on_stop: callbacks.fetch(:on_stop),
|
|
106
|
+
on_refresh: callbacks.fetch(:on_refresh),
|
|
107
|
+
on_switch_theme: callbacks.fetch(:on_switch_theme),
|
|
108
|
+
on_project_change: callbacks.fetch(:on_project_change)
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Event/runtime behavior extracted from MainWindow.
|
|
6
|
+
module MainWindowRuntime
|
|
7
|
+
include MainWindowRuntimeArchiveHelpers
|
|
8
|
+
include MainWindowRuntimeEntryTaskHelpers
|
|
9
|
+
include MainWindowRuntimeEntryTimeHelpers
|
|
10
|
+
include MainWindowRuntimeKeyHelpers
|
|
11
|
+
include MainWindowRuntimeRenderHelpers
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def on_tick
|
|
16
|
+
return unless window.is_visible
|
|
17
|
+
return close if @shutdown_requested
|
|
18
|
+
|
|
19
|
+
now = Time.now
|
|
20
|
+
update_live_indicators(now)
|
|
21
|
+
refresh_if_needed
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update_live_indicators(now)
|
|
25
|
+
controls.clock_label.set_text(now.strftime('%a %d %b %Y %H:%M:%S'))
|
|
26
|
+
controls.timer_label.set_text(view_model.running_timer_line(now: now))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def refresh_if_needed
|
|
30
|
+
return unless @pending_refresh
|
|
31
|
+
|
|
32
|
+
view_model.refresh!
|
|
33
|
+
render!(sync_sheet: true)
|
|
34
|
+
@pending_refresh = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_start(task_input, project_name)
|
|
38
|
+
task_name = resolved_start_task(task_input)
|
|
39
|
+
project = resolved_start_project(project_name)
|
|
40
|
+
view_model.current_project_name = project
|
|
41
|
+
view_model.current_task_input = task_name
|
|
42
|
+
view_model.start_tracking(view_model.sheet_for_task_input(task_name))
|
|
43
|
+
@pending_refresh = true
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
warn("[qtimetrap] start failed: #{e.class}: #{e.message}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def handle_stop
|
|
49
|
+
view_model.stop_tracking
|
|
50
|
+
@pending_refresh = true
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
warn("[qtimetrap] stop failed: #{e.class}: #{e.message}")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_project_selected(projects, project)
|
|
56
|
+
update_current_fields = !view_model.running_current_sheet?
|
|
57
|
+
view_model.select_projects(projects, primary_project: project, sync_current_fields: update_current_fields)
|
|
58
|
+
view_model.current_project_name = project if update_current_fields && project != '* ALL'
|
|
59
|
+
render!
|
|
60
|
+
return unless update_current_fields
|
|
61
|
+
|
|
62
|
+
controls.update_task_input(view_model.current_sheet_input)
|
|
63
|
+
controls.update_project_input(project == '* ALL' ? '' : project)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def handle_task_selected(tasks, task)
|
|
67
|
+
view_model.select_tasks(tasks)
|
|
68
|
+
unless view_model.running_current_sheet?
|
|
69
|
+
view_model.current_task_input = task.to_s
|
|
70
|
+
controls.update_task_input(view_model.current_sheet_input)
|
|
71
|
+
end
|
|
72
|
+
render_controls(sync_sheet: false)
|
|
73
|
+
entries.render(view_model.entry_nodes)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def handle_project_input(project_name)
|
|
77
|
+
return if view_model.running_current_sheet?
|
|
78
|
+
|
|
79
|
+
view_model.current_project_name = project_name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def handle_time_range_changed(from_at, to_at)
|
|
83
|
+
view_model.update_time_range_filter(from_at: from_at, to_at: to_at)
|
|
84
|
+
render_controls(sync_sheet: false)
|
|
85
|
+
entries.update_time_range_inputs(
|
|
86
|
+
from_at: view_model.time_filter_from_at,
|
|
87
|
+
to_at: view_model.time_filter_to_at
|
|
88
|
+
)
|
|
89
|
+
entries.render(view_model.entry_nodes)
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
warn("[qtimetrap] update time-range failed: #{e.class}: #{e.message}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def handle_entry_note_changed(entry_id, note)
|
|
95
|
+
view_model.update_entry_note(entry_id, note)
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
warn("[qtimetrap] update note failed: #{e.class}: #{e.message}")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def resolved_start_task(fallback_task)
|
|
101
|
+
value = controls.task_input.text.to_s.strip
|
|
102
|
+
return value unless value.empty?
|
|
103
|
+
|
|
104
|
+
fallback_task.to_s.strip
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def resolved_start_project(fallback)
|
|
108
|
+
value = controls.project_input.text.to_s.strip
|
|
109
|
+
return value unless value.empty?
|
|
110
|
+
|
|
111
|
+
fallback.to_s.strip
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def switch_theme!
|
|
115
|
+
@theme = theme.with_name(next_theme_name)
|
|
116
|
+
apply_theme
|
|
117
|
+
render!
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
warn("[qtimetrap] save theme failed: #{e.class}: #{e.message}")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Archive handlers for MainWindow runtime.
|
|
6
|
+
module MainWindowRuntimeArchiveHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def handle_archive_mode_toggled(enabled)
|
|
10
|
+
view_model.archive_mode = enabled
|
|
11
|
+
render!(sync_sheet: false)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def handle_entry_archived(entry_id)
|
|
15
|
+
archive_mode_active? ? view_model.unarchive_entry(entry_id) : view_model.archive_entry(entry_id)
|
|
16
|
+
@pending_refresh = true
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
warn("[qtimetrap] archive entry failed: #{e.class}: #{e.message}")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def archive_mode_active?
|
|
22
|
+
view_model.archive_mode?
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Entry task move handlers for MainWindow runtime.
|
|
6
|
+
module MainWindowRuntimeEntryTaskHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def handle_entry_task_changed(entry_id, task_name)
|
|
10
|
+
view_model.update_entry_task(entry_id, task_name)
|
|
11
|
+
@pending_refresh = true
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
warn("[qtimetrap] update task failed: #{e.class}: #{e.message}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def task_suggestions_for_project(project_name)
|
|
17
|
+
view_model.task_names_for_project(project_name)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Entry time change runtime behavior for MainWindow.
|
|
6
|
+
module MainWindowRuntimeEntryTimeHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def handle_entry_time_changed(entry_id, start_text, end_text)
|
|
10
|
+
view_model.update_entry_time(entry_id, start_text, end_text)
|
|
11
|
+
@pending_refresh = true
|
|
12
|
+
rescue StandardError => e
|
|
13
|
+
warn("[qtimetrap] update time failed: #{e.class}: #{e.message}")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Keyboard shortcut handlers for MainWindow runtime.
|
|
6
|
+
module MainWindowRuntimeKeyHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def on_key_press(event)
|
|
10
|
+
key = extract_event_value(event, :a) || 0
|
|
11
|
+
modifiers = extract_event_value(event, :b) || 0
|
|
12
|
+
ctrl_mask = Qt::ControlModifier
|
|
13
|
+
quit_key = Qt::Key_Q
|
|
14
|
+
request_shutdown if modifiers.anybits?(ctrl_mask) && key == quit_key
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def on_space_shortcut
|
|
18
|
+
return if active_line_edit?
|
|
19
|
+
|
|
20
|
+
toggle_tracking_via_space
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_mouse_button_press(event, source_widget: window)
|
|
24
|
+
focused = window.focus_widget
|
|
25
|
+
return unless editable_line_edit?(focused)
|
|
26
|
+
|
|
27
|
+
target = click_target_widget(source_widget, event)
|
|
28
|
+
return if editable_line_edit?(target)
|
|
29
|
+
|
|
30
|
+
focused.clear_focus
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def active_line_edit?
|
|
34
|
+
focused = window.focus_widget
|
|
35
|
+
editable_line_edit?(focused)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def toggle_tracking_via_space
|
|
39
|
+
if view_model.running_current_sheet?
|
|
40
|
+
handle_stop
|
|
41
|
+
else
|
|
42
|
+
handle_start(controls.task_input.text.to_s, controls.project_input.text.to_s)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def click_target_widget(source_widget, event)
|
|
47
|
+
source_widget.child_at(extract_event_value(event, :a), extract_event_value(event, :b))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def editable_line_edit?(widget)
|
|
51
|
+
widget.is_a?(QLineEdit) && !widget.is_read_only
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Rendering helpers for MainWindow runtime loop and interactions.
|
|
6
|
+
module MainWindowRuntimeRenderHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def render!(sync_sheet: false)
|
|
10
|
+
render_sidebar
|
|
11
|
+
render_controls(sync_sheet: sync_sheet)
|
|
12
|
+
render_entries_panel
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_sidebar
|
|
16
|
+
sidebar.render(
|
|
17
|
+
projects: view_model.project_names,
|
|
18
|
+
tasks: view_model.task_names_for_selected_project,
|
|
19
|
+
selection: {
|
|
20
|
+
selected_project: view_model.selected_project,
|
|
21
|
+
selected_projects: view_model.selected_projects
|
|
22
|
+
},
|
|
23
|
+
selected_task: view_model.selected_tasks.first,
|
|
24
|
+
archive_mode: view_model.archive_mode?
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_entries_panel
|
|
29
|
+
entries.update_time_range_inputs(
|
|
30
|
+
from_at: view_model.time_filter_from_at,
|
|
31
|
+
to_at: view_model.time_filter_to_at
|
|
32
|
+
)
|
|
33
|
+
entries.render(view_model.entry_nodes)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render_controls(sync_sheet:)
|
|
37
|
+
controls.update_summary(view_model.summary_line)
|
|
38
|
+
update_tracking_controls(sync_sheet: sync_sheet)
|
|
39
|
+
controls.update_theme_label(theme.name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update_tracking_controls(sync_sheet:)
|
|
43
|
+
controls.update_task_input(view_model.current_sheet_input) if sync_sheet
|
|
44
|
+
controls.update_action_button(running: view_model.running_current_sheet?)
|
|
45
|
+
project_name = view_model.current_project_name.to_s
|
|
46
|
+
controls.update_project_input(project_name) unless project_name.strip.empty?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Bootstrap helpers to stabilize initial splitter toggle placement.
|
|
6
|
+
module MainWindowSplitterToggleBootstrapHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def schedule_initial_toggle_reposition(splitter:, sidebar_widget:, button:, zone:)
|
|
10
|
+
attempts = { count: 0 }
|
|
11
|
+
context = { splitter: splitter, sidebar_widget: sidebar_widget, button: button, zone: zone }
|
|
12
|
+
timer = QTimer.new(zone)
|
|
13
|
+
timer.set_interval(120)
|
|
14
|
+
timer.connect('timeout') do |_|
|
|
15
|
+
tick_initial_reposition(
|
|
16
|
+
attempts: attempts,
|
|
17
|
+
timer: timer,
|
|
18
|
+
context: context
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
timer.start
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def tick_initial_reposition(attempts:, timer:, context:)
|
|
25
|
+
attempts[:count] += 1
|
|
26
|
+
reposition_toggle_affordance(**context)
|
|
27
|
+
return unless attempts[:count] >= 4
|
|
28
|
+
|
|
29
|
+
timer.stop
|
|
30
|
+
timer.delete_later
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QTimetrap
|
|
4
|
+
module Views
|
|
5
|
+
# Sidebar collapse/expand toggle attached to splitter handle area.
|
|
6
|
+
module MainWindowSplitterToggleHelpers
|
|
7
|
+
include MainWindowSplitterToggleLayoutHelpers
|
|
8
|
+
include MainWindowSplitterToggleBootstrapHelpers
|
|
9
|
+
include MainWindowSplitterToggleHoverHelpers
|
|
10
|
+
|
|
11
|
+
SIDEBAR_TOGGLE_W = 20
|
|
12
|
+
SIDEBAR_TOGGLE_H = 56
|
|
13
|
+
SIDEBAR_TOGGLE_ZONE_W = 24
|
|
14
|
+
SIDEBAR_TOGGLE_ZONE_H = 128
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def add_sidebar_toggle_button(window:, splitter:, sidebar_widget:)
|
|
19
|
+
button = build_sidebar_toggle_button(window)
|
|
20
|
+
zone = build_sidebar_toggle_zone(window)
|
|
21
|
+
state = { collapsed: false, button_hovered: false, zone_hovered: false }
|
|
22
|
+
button.connect('clicked') { |_| on_sidebar_toggle_clicked(splitter, sidebar_widget, button, zone, state) }
|
|
23
|
+
bind_splitter_toggle_events(
|
|
24
|
+
splitter: splitter,
|
|
25
|
+
sidebar_widget: sidebar_widget,
|
|
26
|
+
button: button,
|
|
27
|
+
zone: zone,
|
|
28
|
+
state: state
|
|
29
|
+
)
|
|
30
|
+
bind_toggle_button_events(button: button, state: state)
|
|
31
|
+
reposition_toggle_affordance(splitter: splitter, sidebar_widget: sidebar_widget, button: button, zone: zone)
|
|
32
|
+
schedule_initial_toggle_reposition(
|
|
33
|
+
splitter: splitter,
|
|
34
|
+
sidebar_widget: sidebar_widget,
|
|
35
|
+
button: button,
|
|
36
|
+
zone: zone
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_sidebar_toggle_button(window)
|
|
41
|
+
QPushButton.new(window).tap do |button|
|
|
42
|
+
button.set_object_name('sidebar_toggle_button')
|
|
43
|
+
button.set_text('◀')
|
|
44
|
+
button.set_focus_policy(Qt::NoFocus)
|
|
45
|
+
button.set_tool_tip('Collapse sidebar')
|
|
46
|
+
button.set_fixed_size(SIDEBAR_TOGGLE_W, SIDEBAR_TOGGLE_H)
|
|
47
|
+
button.raise
|
|
48
|
+
button.hide
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_sidebar_toggle_clicked(splitter, sidebar_widget, button, zone, state)
|
|
53
|
+
state[:collapsed] = !state[:collapsed]
|
|
54
|
+
update_sidebar_visibility(sidebar_widget, button, collapsed: state[:collapsed])
|
|
55
|
+
reposition_toggle_affordance(splitter: splitter, sidebar_widget: sidebar_widget, button: button, zone: zone)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_sidebar_toggle_zone(window)
|
|
59
|
+
QWidget.new(window).tap do |zone|
|
|
60
|
+
zone.set_object_name('sidebar_toggle_hotspot')
|
|
61
|
+
zone.set_fixed_size(SIDEBAR_TOGGLE_ZONE_W, SIDEBAR_TOGGLE_ZONE_H)
|
|
62
|
+
zone.set_style_sheet('background: transparent;')
|
|
63
|
+
zone.raise
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def bind_splitter_toggle_events(splitter:, sidebar_widget:, button:, zone:, state:)
|
|
68
|
+
splitter.connect('splitterMoved') do |_|
|
|
69
|
+
reposition_toggle_affordance(splitter: splitter, sidebar_widget: sidebar_widget, button: button, zone: zone)
|
|
70
|
+
end
|
|
71
|
+
splitter.on(:resize) do |_|
|
|
72
|
+
reposition_toggle_affordance(splitter: splitter, sidebar_widget: sidebar_widget, button: button, zone: zone)
|
|
73
|
+
end
|
|
74
|
+
sidebar_widget.on(:resize) do |_|
|
|
75
|
+
reposition_toggle_affordance(splitter: splitter, sidebar_widget: sidebar_widget, button: button, zone: zone)
|
|
76
|
+
end
|
|
77
|
+
bind_toggle_zone_events(zone: zone, button: button, state: state)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reposition_toggle_affordance(splitter:, sidebar_widget:, button:, zone:)
|
|
81
|
+
x = toggle_x(splitter: splitter, sidebar_widget: sidebar_widget)
|
|
82
|
+
y = toggle_y(splitter_height: splitter.height)
|
|
83
|
+
button.move(x, y)
|
|
84
|
+
button.raise
|
|
85
|
+
zone_x = x - ((SIDEBAR_TOGGLE_ZONE_W - SIDEBAR_TOGGLE_W) / 2)
|
|
86
|
+
zone_y = y - ((SIDEBAR_TOGGLE_ZONE_H - SIDEBAR_TOGGLE_H) / 2)
|
|
87
|
+
zone.move(zone_x, zone_y)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def update_sidebar_visibility(sidebar_widget, button, collapsed:)
|
|
91
|
+
if collapsed
|
|
92
|
+
sidebar_widget.hide
|
|
93
|
+
button.set_text('▶')
|
|
94
|
+
button.set_tool_tip('Expand sidebar')
|
|
95
|
+
else
|
|
96
|
+
sidebar_widget.show
|
|
97
|
+
button.set_text('◀')
|
|
98
|
+
button.set_tool_tip('Collapse sidebar')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|