rukbat 0.1.0
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/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +100 -0
- data/exe/rukbat +37 -0
- data/lib/rukbat/application.rb +37 -0
- data/lib/rukbat/atomic_file.rb +11 -0
- data/lib/rukbat/cell_source.rb +25 -0
- data/lib/rukbat/csv_file.rb +125 -0
- data/lib/rukbat/grid_view.rb +847 -0
- data/lib/rukbat/pdf_file.rb +163 -0
- data/lib/rukbat/version.rb +5 -0
- data/lib/rukbat/workbook.rb +1045 -0
- data/lib/rukbat.rb +23 -0
- data/sig/rukbat.rbs +118 -0
- metadata +201 -0
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rukbat
|
|
4
|
+
class GridView < Zaniah::UI::Component
|
|
5
|
+
UI = Zaniah::UI
|
|
6
|
+
MAX_FILL_CELLS = 100_000
|
|
7
|
+
|
|
8
|
+
attr_reader :grid, :formula_field, :active_cell, :status, :completion_session,
|
|
9
|
+
:completion_candidate
|
|
10
|
+
|
|
11
|
+
def initialize(workbook, on_save: nil)
|
|
12
|
+
super()
|
|
13
|
+
@workbook, @on_save = workbook, on_save
|
|
14
|
+
@active_cell, @status = [1, 1], "Ready"
|
|
15
|
+
@rendered_cells = {}
|
|
16
|
+
@editing_cell = @inline_buffer = @inline_editor = nil
|
|
17
|
+
@inline_focus_pending = false
|
|
18
|
+
@formula_field = UI::TextField.new("")
|
|
19
|
+
@formula_field.on_change do |text, cx|
|
|
20
|
+
update_completion(text)
|
|
21
|
+
@inline_buffer.replace(0...@inline_buffer.bytesize, text.encode(Encoding::UTF_8)) if @inline_buffer
|
|
22
|
+
cx.window.request_frame
|
|
23
|
+
end
|
|
24
|
+
@function_index = Spica::Index.new(Furud::Functions.standard.names)
|
|
25
|
+
@completion_session = @function_index.session
|
|
26
|
+
@completion_button = UI::Button.new("Use completion", size: :sm, variant: :secondary)
|
|
27
|
+
.on_click { apply_completion }
|
|
28
|
+
@apply_button = UI::Button.new("Apply", size: :sm).on_click { apply_edit }
|
|
29
|
+
@save_button = UI::Button.new("Save", size: :sm, variant: :secondary).on_click { save }
|
|
30
|
+
@undo_button = UI::Button.new("Undo", size: :sm, variant: :ghost).on_click { undo }
|
|
31
|
+
@redo_button = UI::Button.new("Redo", size: :sm, variant: :ghost).on_click { redo }
|
|
32
|
+
@add_sheet_button = UI::Button.new("+ Sheet", size: :sm, variant: :secondary).on_click { add_sheet }
|
|
33
|
+
@number_button = UI::Button.new("#,##0.00", size: :sm, variant: :ghost).on_click { apply_format(number_format: "#,##0.00") }
|
|
34
|
+
@percent_button = UI::Button.new("%", size: :sm, variant: :ghost).on_click { apply_format(number_format: "0.00%") }
|
|
35
|
+
@bold_button = UI::Button.new("Bold", size: :sm, variant: :ghost).on_click { toggle_bold }
|
|
36
|
+
@fill_button = UI::Button.new("Fill", size: :sm, variant: :ghost).on_click { apply_format(background: "#FFF2CC") }
|
|
37
|
+
@freeze_button = UI::Button.new("Freeze", size: :sm, variant: :ghost).on_click { freeze_panes }
|
|
38
|
+
@unfreeze_button = UI::Button.new("Unfreeze", size: :sm, variant: :ghost).on_click { unfreeze_panes }
|
|
39
|
+
@clear_conditional_button = UI::Button.new("Clear highlights", size: :sm, variant: :ghost).on_click { clear_highlights }
|
|
40
|
+
@line_chart_button = UI::Button.new("Line", size: :sm, variant: :ghost).on_click { show_chart(:line) }
|
|
41
|
+
@bar_chart_button = UI::Button.new("Bar", size: :sm, variant: :ghost).on_click { show_chart(:bar) }
|
|
42
|
+
@pie_chart_button = UI::Button.new("Pie", size: :sm, variant: :ghost).on_click { show_chart(:pie) }
|
|
43
|
+
@font_down_button = UI::Button.new("A−", size: :sm, variant: :ghost).on_click { change_font_size(-1) }
|
|
44
|
+
@font_up_button = UI::Button.new("A+", size: :sm, variant: :ghost).on_click { change_font_size(1) }
|
|
45
|
+
@align_button = UI::Button.new("Align", size: :sm, variant: :ghost).on_click { cycle_alignment }
|
|
46
|
+
@text_color_button = UI::Button.new("Text color", size: :sm, variant: :ghost).on_click { apply_format(color: "#C00000") }
|
|
47
|
+
@border_button = UI::Button.new("Border", size: :sm, variant: :ghost).on_click do
|
|
48
|
+
apply_format(border_color: "#7F8793", border_width: 1)
|
|
49
|
+
end
|
|
50
|
+
@sort_button = UI::Button.new("Sort ↑", size: :sm, variant: :ghost).on_click { sort_selection }
|
|
51
|
+
@filter_field = UI::TextField.new("")
|
|
52
|
+
@filter_button = UI::Button.new("Filter", size: :sm, variant: :ghost).on_click { apply_filter }
|
|
53
|
+
@clear_filter_button = UI::Button.new("Show all", size: :sm, variant: :ghost).on_click { clear_filter }
|
|
54
|
+
@remove_duplicates_button = UI::Button.new("Unique", size: :sm, variant: :ghost).on_click { remove_duplicates }
|
|
55
|
+
@hide_rows_button = UI::Button.new("Hide rows", size: :sm, variant: :ghost).on_click { hide_selected_rows }
|
|
56
|
+
@hide_columns_button = UI::Button.new("Hide cols", size: :sm, variant: :ghost).on_click { hide_selected_columns }
|
|
57
|
+
@show_hidden_button = UI::Button.new("Show hidden", size: :sm, variant: :ghost).on_click { show_hidden }
|
|
58
|
+
@insert_row_button = UI::Button.new("Insert row", size: :sm, variant: :ghost).on_click { edit_structure(:insert_rows) }
|
|
59
|
+
@delete_rows_button = UI::Button.new("Delete rows", size: :sm, variant: :ghost).on_click { edit_structure(:delete_rows) }
|
|
60
|
+
@insert_column_button = UI::Button.new("Insert col", size: :sm, variant: :ghost).on_click { edit_structure(:insert_columns) }
|
|
61
|
+
@delete_columns_button = UI::Button.new("Delete cols", size: :sm, variant: :ghost).on_click { edit_structure(:delete_columns) }
|
|
62
|
+
@set_print_area_button = UI::Button.new("Set print area", size: :sm, variant: :ghost).on_click { set_print_area }
|
|
63
|
+
@clear_print_area_button = UI::Button.new("Clear print area", size: :sm, variant: :ghost).on_click { clear_print_area }
|
|
64
|
+
@export_pdf_button = UI::Button.new("Export PDF", size: :sm, variant: :ghost).on_click { export_pdf }
|
|
65
|
+
@find_field = UI::TextField.new("")
|
|
66
|
+
@replace_field = UI::TextField.new("")
|
|
67
|
+
@find_button = UI::Button.new("Find", size: :sm, variant: :ghost).on_click { find_selection }
|
|
68
|
+
@replace_button = UI::Button.new("Replace", size: :sm, variant: :ghost).on_click { replace_selection }
|
|
69
|
+
@comment_field = UI::TextField.new("")
|
|
70
|
+
@comment_button = UI::Button.new("Comment", size: :sm, variant: :ghost).on_click { save_comment }
|
|
71
|
+
@name_field = UI::TextField.new("")
|
|
72
|
+
@name_button = UI::Button.new("Name range", size: :sm, variant: :ghost).on_click { name_selection }
|
|
73
|
+
@highlight_button = UI::Button.new("> 0", size: :sm, variant: :ghost).on_click { highlight_positive }
|
|
74
|
+
@sort_ascending = true
|
|
75
|
+
@filter_hidden_rows = []
|
|
76
|
+
@applied_hidden_rows = []
|
|
77
|
+
@applied_hidden_columns = []
|
|
78
|
+
@change_observer = @workbook.on_cells_changed do |sheet, cells|
|
|
79
|
+
invalidate_rendered_cells(sheet, cells)
|
|
80
|
+
request_frame if sheet.nil? || sheet == @workbook.active_sheet
|
|
81
|
+
end
|
|
82
|
+
frozen_rows, frozen_columns = @workbook.frozen_panes
|
|
83
|
+
@grid = UI::Grid.new(rows: Workbook::MAX_ROWS + 1, columns: Workbook::MAX_COLUMNS + 1,
|
|
84
|
+
row_height: 24, column_width: ->(index) { index.zero? ? 52 : 112 },
|
|
85
|
+
frozen_rows: frozen_rows, frozen_columns: frozen_columns) do |row, column, bounds, _cx|
|
|
86
|
+
render_cell(row, column, bounds)
|
|
87
|
+
end
|
|
88
|
+
@grid.on_select { |areas, _event, cx| selection_changed(areas, cx) }
|
|
89
|
+
@grid.on_edit { |row, column, _event, cx| begin_edit(row, column, cx) }
|
|
90
|
+
@grid.on_fill { |source, target, _cx| fill(source, target) }
|
|
91
|
+
sync_grid_visibility
|
|
92
|
+
sync_formula_field
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def request_layout(cx)
|
|
96
|
+
@rendered_cells_used = {}
|
|
97
|
+
layout = super
|
|
98
|
+
if @inline_focus_pending && @inline_editor&.focus_handle
|
|
99
|
+
cx.dispatcher.focus(@inline_editor.focus_handle)
|
|
100
|
+
@inline_focus_pending = false
|
|
101
|
+
end
|
|
102
|
+
layout
|
|
103
|
+
ensure
|
|
104
|
+
if @rendered_cells_used
|
|
105
|
+
@rendered_cells.delete_if { |key, _cell| !@rendered_cells_used.key?(key) }
|
|
106
|
+
@rendered_cells_used = nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build(cx)
|
|
111
|
+
@cx = cx
|
|
112
|
+
render_context = [cx.theme, cx.text_system]
|
|
113
|
+
if @render_context != render_context
|
|
114
|
+
@rendered_cells.clear
|
|
115
|
+
@render_context = render_context
|
|
116
|
+
end
|
|
117
|
+
toolbar = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
118
|
+
.child(@undo_button).child(@redo_button).child(@save_button).child(@add_sheet_button)
|
|
119
|
+
.child(@number_button).child(@percent_button).child(@bold_button).child(@fill_button)
|
|
120
|
+
.child(@freeze_button).child(@unfreeze_button).child(@clear_conditional_button)
|
|
121
|
+
.child(@line_chart_button).child(@bar_chart_button).child(@pie_chart_button)
|
|
122
|
+
format_toolbar = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
123
|
+
.child(@font_down_button).child(@font_up_button).child(@align_button)
|
|
124
|
+
.child(@text_color_button).child(@border_button)
|
|
125
|
+
data_toolbar = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
126
|
+
.child(@sort_button).child(@remove_duplicates_button)
|
|
127
|
+
.child(@filter_field.style(width: 120)).child(@filter_button).child(@clear_filter_button)
|
|
128
|
+
.child(@hide_rows_button).child(@hide_columns_button).child(@show_hidden_button)
|
|
129
|
+
structure_toolbar = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
130
|
+
.child(@insert_row_button).child(@delete_rows_button)
|
|
131
|
+
.child(@insert_column_button).child(@delete_columns_button)
|
|
132
|
+
.child(@set_print_area_button).child(@clear_print_area_button).child(@export_pdf_button)
|
|
133
|
+
annotation_toolbar = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
134
|
+
.child(@highlight_button).child(@comment_field.style(width: 150)).child(@comment_button)
|
|
135
|
+
.child(@name_field.style(width: 120)).child(@name_button)
|
|
136
|
+
find_toolbar = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
137
|
+
.child(@find_field.style(width: 140)).child(@find_button)
|
|
138
|
+
.child(@replace_field.style(width: 140)).child(@replace_button)
|
|
139
|
+
sheets = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
140
|
+
@workbook.sheet_names.each do |name|
|
|
141
|
+
variant = name == @workbook.active_sheet ? :secondary : :ghost
|
|
142
|
+
sheets.child(UI::Button.new(name, size: :sm, variant: variant).on_click do
|
|
143
|
+
clear_filter
|
|
144
|
+
@workbook.activate(name)
|
|
145
|
+
sync_grid_visibility
|
|
146
|
+
sync_frozen_panes
|
|
147
|
+
sync_formula_field
|
|
148
|
+
update_status
|
|
149
|
+
end)
|
|
150
|
+
end
|
|
151
|
+
formula_row = Zaniah::Div.new.flex_row.items_center.gap(cx.theme.spacing[1])
|
|
152
|
+
.child(UI::Label.new(cell_address(*@active_cell), size: :sm))
|
|
153
|
+
.child(@formula_field.style(flex_grow: 1, flex_basis: 0))
|
|
154
|
+
.child(UI::Label.new(@completion_candidate || "", tone: :muted, size: :sm))
|
|
155
|
+
.child(@completion_button.disabled(@completion_candidate.nil?))
|
|
156
|
+
.child(@apply_button)
|
|
157
|
+
status_row = Zaniah::Div.new.flex_row.items_center.style(justify_content: :space_between)
|
|
158
|
+
.child(UI::Label.new(@status, tone: :muted, size: :sm)).child(sheets)
|
|
159
|
+
content = Zaniah::Div.new.flex_col.gap(cx.theme.spacing[1]).p(cx.theme.spacing[2])
|
|
160
|
+
.style(width: percent(100), height: percent(100))
|
|
161
|
+
.child(toolbar).child(format_toolbar).child(data_toolbar).child(structure_toolbar).child(annotation_toolbar)
|
|
162
|
+
.child(find_toolbar).child(formula_row).child(@grid.flex_1)
|
|
163
|
+
content.child(@chart_component) if @chart_component
|
|
164
|
+
content.child(status_row)
|
|
165
|
+
content
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def undo
|
|
169
|
+
@status = @workbook.undo ? "Undone" : "Nothing to undo"
|
|
170
|
+
sync_grid_visibility
|
|
171
|
+
sync_frozen_panes
|
|
172
|
+
sync_formula_field
|
|
173
|
+
request_frame
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def redo
|
|
177
|
+
@status = @workbook.redo ? "Redone" : "Nothing to redo"
|
|
178
|
+
sync_grid_visibility
|
|
179
|
+
sync_frozen_panes
|
|
180
|
+
sync_formula_field
|
|
181
|
+
request_frame
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def handle_shortcut(event, window)
|
|
185
|
+
return false unless event.is_a?(Zaniah::Input::KeyDown)
|
|
186
|
+
|
|
187
|
+
if @editing_cell
|
|
188
|
+
case event.keystroke.to_s
|
|
189
|
+
when "enter" then return commit_inline_edit
|
|
190
|
+
when "esc", "escape" then return cancel_inline_edit
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
case event.keystroke.to_s
|
|
195
|
+
when "ctrl-s", "cmd-s" then save
|
|
196
|
+
when "ctrl-z", "cmd-z"
|
|
197
|
+
return false if focused_text_field?(window)
|
|
198
|
+
undo
|
|
199
|
+
when "ctrl-shift-z", "cmd-shift-z"
|
|
200
|
+
return false if focused_text_field?(window)
|
|
201
|
+
self.redo
|
|
202
|
+
else return false
|
|
203
|
+
end
|
|
204
|
+
true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def apply_edit
|
|
208
|
+
if @editing_cell
|
|
209
|
+
@inline_buffer.replace(0...@inline_buffer.bytesize, @formula_field.value.encode(Encoding::UTF_8))
|
|
210
|
+
return commit_inline_edit
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
value = CSVFile.parse_cell(@formula_field.value)
|
|
214
|
+
value.nil? ? @workbook.clear(*@active_cell) : @workbook.set(*@active_cell, value)
|
|
215
|
+
@status = "Applied #{cell_address(*@active_cell)}"
|
|
216
|
+
sync_formula_field
|
|
217
|
+
request_frame
|
|
218
|
+
true
|
|
219
|
+
rescue Rukbat::Error, ArgumentError, TypeError => error
|
|
220
|
+
@status = error.message
|
|
221
|
+
request_frame
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def sort_selection
|
|
228
|
+
clear_filter
|
|
229
|
+
top, left, bottom, right = selected_coordinates
|
|
230
|
+
by = left
|
|
231
|
+
@workbook.sort(top, left, bottom, right, by: by, ascending: @sort_ascending)
|
|
232
|
+
@sort_ascending = !@sort_ascending
|
|
233
|
+
@sort_button.text = @sort_ascending ? "Sort ↑" : "Sort ↓" if @sort_button.respond_to?(:text=)
|
|
234
|
+
@status = "Sorted #{cell_address(top, left)}:#{cell_address(bottom, right)}"
|
|
235
|
+
sync_formula_field
|
|
236
|
+
request_frame
|
|
237
|
+
true
|
|
238
|
+
rescue Rukbat::Error => error
|
|
239
|
+
@status = error.message
|
|
240
|
+
request_frame
|
|
241
|
+
false
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def apply_filter
|
|
245
|
+
top, left, bottom, = selected_coordinates
|
|
246
|
+
clear_filter
|
|
247
|
+
return true if top > bottom
|
|
248
|
+
|
|
249
|
+
rows = @workbook.filter_rows(top, bottom, by: left, query: @filter_field.value)
|
|
250
|
+
matching = rows.to_h { |row| [row, true] }
|
|
251
|
+
@filter_hidden_rows = (top..bottom).reject { |row| matching.key?(row) }
|
|
252
|
+
@grid.hide_rows(@filter_hidden_rows, hidden: true) unless @filter_hidden_rows.empty?
|
|
253
|
+
@status = "Showing #{rows.length} matching rows"
|
|
254
|
+
request_frame
|
|
255
|
+
true
|
|
256
|
+
rescue Rukbat::Error, ArgumentError => error
|
|
257
|
+
@status = error.message
|
|
258
|
+
request_frame
|
|
259
|
+
false
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def clear_filter
|
|
263
|
+
@grid.hide_rows(@filter_hidden_rows, hidden: false) unless @filter_hidden_rows.empty?
|
|
264
|
+
@filter_hidden_rows = []
|
|
265
|
+
sync_grid_visibility
|
|
266
|
+
@status = "Filter cleared"
|
|
267
|
+
request_frame
|
|
268
|
+
true
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def remove_duplicates
|
|
272
|
+
clear_filter
|
|
273
|
+
top, left, bottom, right = selected_coordinates
|
|
274
|
+
removed = @workbook.remove_duplicates(top, left, bottom, right)
|
|
275
|
+
sync_grid_visibility
|
|
276
|
+
sync_formula_field
|
|
277
|
+
@status = "Removed #{removed} duplicate rows"
|
|
278
|
+
request_frame
|
|
279
|
+
true
|
|
280
|
+
rescue Rukbat::Error => error
|
|
281
|
+
@status = error.message
|
|
282
|
+
request_frame
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def find_selection
|
|
287
|
+
top, left, bottom, right = selected_coordinates
|
|
288
|
+
matches = @workbook.find(@find_field.value, top: top, left: left, bottom: bottom, right: right)
|
|
289
|
+
@grid.selection = matches.map do |reference|
|
|
290
|
+
UI::Grid::Area.new(rows: reference.row...(reference.row + 1),
|
|
291
|
+
columns: reference.column...(reference.column + 1))
|
|
292
|
+
end
|
|
293
|
+
@active_cell = [matches.first.row, matches.first.column] unless matches.empty?
|
|
294
|
+
sync_formula_field unless matches.empty?
|
|
295
|
+
@status = "Found #{matches.length} cells"
|
|
296
|
+
request_frame
|
|
297
|
+
true
|
|
298
|
+
rescue Rukbat::Error => error
|
|
299
|
+
@status = error.message
|
|
300
|
+
request_frame
|
|
301
|
+
false
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def replace_selection
|
|
305
|
+
top, left, bottom, right = selected_coordinates
|
|
306
|
+
count = @workbook.replace_all(@find_field.value, @replace_field.value,
|
|
307
|
+
top: top, left: left, bottom: bottom, right: right)
|
|
308
|
+
sync_formula_field
|
|
309
|
+
@status = "Replaced #{count} cells"
|
|
310
|
+
request_frame
|
|
311
|
+
true
|
|
312
|
+
rescue Rukbat::Error => error
|
|
313
|
+
@status = error.message
|
|
314
|
+
request_frame
|
|
315
|
+
false
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def save_comment
|
|
319
|
+
@workbook.set_comment(*@active_cell, @comment_field.value)
|
|
320
|
+
@status = "Comment saved for #{cell_address(*@active_cell)}"
|
|
321
|
+
request_frame
|
|
322
|
+
true
|
|
323
|
+
rescue Rukbat::Error => error
|
|
324
|
+
@status = error.message
|
|
325
|
+
request_frame
|
|
326
|
+
false
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def name_selection
|
|
330
|
+
top, left, bottom, right = selected_coordinates
|
|
331
|
+
@workbook.define_name(@name_field.value, top, left, bottom, right)
|
|
332
|
+
@status = "Named range #{@name_field.value}"
|
|
333
|
+
request_frame
|
|
334
|
+
true
|
|
335
|
+
rescue Rukbat::Error => error
|
|
336
|
+
@status = error.message
|
|
337
|
+
request_frame
|
|
338
|
+
false
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def change_font_size(delta)
|
|
342
|
+
current = @workbook.format_at(*@active_cell).fetch(:font_size, 12)
|
|
343
|
+
apply_format(font_size: (current + delta).clamp(6, 72))
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def cycle_alignment
|
|
347
|
+
current = @workbook.format_at(*@active_cell).fetch(:horizontal_alignment, :left)
|
|
348
|
+
alignment = {left: :center, center: :right, right: :left}.fetch(current, :left)
|
|
349
|
+
apply_format(horizontal_alignment: alignment)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def hide_selected_rows
|
|
353
|
+
top, _left, bottom, = selected_coordinates
|
|
354
|
+
@workbook.hide_rows(top, bottom)
|
|
355
|
+
sync_grid_visibility
|
|
356
|
+
@status = "Hidden rows #{top}-#{bottom}"
|
|
357
|
+
request_frame
|
|
358
|
+
true
|
|
359
|
+
rescue Rukbat::Error => error
|
|
360
|
+
@status = error.message
|
|
361
|
+
request_frame
|
|
362
|
+
false
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def hide_selected_columns
|
|
366
|
+
_top, left, _bottom, right = selected_coordinates
|
|
367
|
+
@workbook.hide_columns(left, right)
|
|
368
|
+
sync_grid_visibility
|
|
369
|
+
@status = "Hidden columns #{left}-#{right}"
|
|
370
|
+
request_frame
|
|
371
|
+
true
|
|
372
|
+
rescue Rukbat::Error => error
|
|
373
|
+
@status = error.message
|
|
374
|
+
request_frame
|
|
375
|
+
false
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def show_hidden
|
|
379
|
+
clear_filter
|
|
380
|
+
@workbook.clear_hidden(:rows)
|
|
381
|
+
@workbook.clear_hidden(:columns)
|
|
382
|
+
sync_grid_visibility
|
|
383
|
+
@status = "All rows and columns are visible"
|
|
384
|
+
request_frame
|
|
385
|
+
true
|
|
386
|
+
rescue Rukbat::Error => error
|
|
387
|
+
@status = error.message
|
|
388
|
+
request_frame
|
|
389
|
+
false
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def edit_structure(operation)
|
|
393
|
+
top, left, bottom, right = selected_coordinates
|
|
394
|
+
if operation == :insert_rows || operation == :delete_rows
|
|
395
|
+
start = top
|
|
396
|
+
count = operation == :insert_rows ? 1 : bottom - top + 1
|
|
397
|
+
else
|
|
398
|
+
start = left
|
|
399
|
+
count = operation == :insert_columns ? 1 : right - left + 1
|
|
400
|
+
end
|
|
401
|
+
@workbook.public_send(operation, start, count)
|
|
402
|
+
sync_grid_visibility
|
|
403
|
+
sync_formula_field
|
|
404
|
+
@status = "#{operation.to_s.tr('_', ' ').capitalize} at #{start} (#{count})"
|
|
405
|
+
request_frame
|
|
406
|
+
true
|
|
407
|
+
rescue Rukbat::Error => error
|
|
408
|
+
@status = error.message
|
|
409
|
+
request_frame
|
|
410
|
+
false
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def set_print_area
|
|
414
|
+
top, left, bottom, right = selected_coordinates
|
|
415
|
+
@workbook.set_print_area(top, left, bottom, right)
|
|
416
|
+
@status = "Print area set to #{cell_address(top, left)}:#{cell_address(bottom, right)}"
|
|
417
|
+
request_frame
|
|
418
|
+
true
|
|
419
|
+
rescue Rukbat::Error => error
|
|
420
|
+
@status = error.message
|
|
421
|
+
request_frame
|
|
422
|
+
false
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def clear_print_area
|
|
426
|
+
@workbook.clear_print_area
|
|
427
|
+
@status = "Print area cleared"
|
|
428
|
+
request_frame
|
|
429
|
+
true
|
|
430
|
+
rescue Rukbat::Error => error
|
|
431
|
+
@status = error.message
|
|
432
|
+
request_frame
|
|
433
|
+
false
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def export_pdf
|
|
437
|
+
window = @cx&.window
|
|
438
|
+
raise Rukbat::Error, "PDF export requires an interactive window" unless window&.respond_to?(:prompt_for_paths)
|
|
439
|
+
|
|
440
|
+
font = window.prompt_for_paths.first
|
|
441
|
+
return false unless font
|
|
442
|
+
path = window.prompt_for_paths(save: true).first
|
|
443
|
+
return false unless path
|
|
444
|
+
path = "#{path}.pdf" if File.extname(path).empty?
|
|
445
|
+
|
|
446
|
+
PDFFile.write(@workbook, path, font: font)
|
|
447
|
+
@status = "Exported #{File.basename(path)}"
|
|
448
|
+
request_frame
|
|
449
|
+
true
|
|
450
|
+
rescue Rukbat::Error, ArgumentError => error
|
|
451
|
+
@status = error.message
|
|
452
|
+
request_frame
|
|
453
|
+
false
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def sync_grid_visibility
|
|
457
|
+
@grid.hide_rows(@applied_hidden_rows, hidden: false) unless @applied_hidden_rows.empty?
|
|
458
|
+
@grid.hide_columns(@applied_hidden_columns, hidden: false) unless @applied_hidden_columns.empty?
|
|
459
|
+
@applied_hidden_rows = @workbook.hidden_rows
|
|
460
|
+
@applied_hidden_columns = @workbook.hidden_columns
|
|
461
|
+
@grid.hide_rows(@applied_hidden_rows, hidden: true) unless @applied_hidden_rows.empty?
|
|
462
|
+
@grid.hide_columns(@applied_hidden_columns, hidden: true) unless @applied_hidden_columns.empty?
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def highlight_positive
|
|
466
|
+
top, left, bottom, right = selected_coordinates
|
|
467
|
+
@workbook.add_conditional_format(top, left, bottom, right, operator: :greater_than,
|
|
468
|
+
value: 0, style: {color: "#008000"})
|
|
469
|
+
@status = "Added positive-value highlight"
|
|
470
|
+
request_frame
|
|
471
|
+
true
|
|
472
|
+
rescue Rukbat::Error => error
|
|
473
|
+
@status = error.message
|
|
474
|
+
request_frame
|
|
475
|
+
false
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def clear_highlights
|
|
479
|
+
@workbook.clear_conditional_formats
|
|
480
|
+
@status = "Conditional formatting cleared"
|
|
481
|
+
request_frame
|
|
482
|
+
true
|
|
483
|
+
rescue Rukbat::Error => error
|
|
484
|
+
@status = error.message
|
|
485
|
+
request_frame
|
|
486
|
+
false
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
private
|
|
490
|
+
|
|
491
|
+
def render_cell(row, column, bounds)
|
|
492
|
+
key = [@workbook.active_sheet, row, column, bounds.height]
|
|
493
|
+
@rendered_cells_used[key] = true if @rendered_cells_used
|
|
494
|
+
return @rendered_cells[key] if @rendered_cells.key?(key)
|
|
495
|
+
|
|
496
|
+
content = if row.zero?
|
|
497
|
+
UI::Label.new(column.zero? ? "" : column_name(column), size: :sm, wrap: :none)
|
|
498
|
+
elsif column.zero?
|
|
499
|
+
UI::Label.new(row.to_s, size: :sm, wrap: :none)
|
|
500
|
+
else
|
|
501
|
+
render_data_cell(row, column, bounds)
|
|
502
|
+
end
|
|
503
|
+
@rendered_cells[key] = content
|
|
504
|
+
rescue Rukbat::Error
|
|
505
|
+
@rendered_cells[key] = UI::Label.new("", size: :sm)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def render_data_cell(row, column, bounds)
|
|
509
|
+
return @inline_editor if @editing_cell == [row, column]
|
|
510
|
+
|
|
511
|
+
text, style = @workbook.presentation_at(row, column)
|
|
512
|
+
comment = @workbook.comment_at(row, column)
|
|
513
|
+
if style.empty? && comment.nil?
|
|
514
|
+
line_height = 12 * 1.4
|
|
515
|
+
return Zaniah::Text.new(text, size: 12, color: @cx.theme.colors.text, wrap: :none)
|
|
516
|
+
.style(margin_top: [(bounds.height - line_height) / 2, 0].max)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
font = if style[:font_family] || style[:bold] || style[:italic]
|
|
520
|
+
@cx.text_system&.font_db&.find(family: style[:font_family],
|
|
521
|
+
weight: style[:bold] ? 700 : 400, style: style[:italic] ? :italic : :normal)
|
|
522
|
+
end
|
|
523
|
+
alignment = {left: :start, center: :center, right: :end}.fetch(style[:horizontal_alignment], :start)
|
|
524
|
+
vertical = {top: :start, middle: :center, bottom: :end}.fetch(style[:vertical_alignment], :center)
|
|
525
|
+
content = Zaniah::Text.new(text, size: style[:font_size] || 12, color: style[:color] || @cx.theme.colors.text,
|
|
526
|
+
font: font, wrap: :none, align: alignment)
|
|
527
|
+
cell = Zaniah::Div.new.flex_row.w_full.h_full.p([1, 4])
|
|
528
|
+
.style(align_items: vertical, background: style[:background] || "#0000",
|
|
529
|
+
border: style[:border_width] || 0, border_color: style[:border_color])
|
|
530
|
+
.child(content)
|
|
531
|
+
cell.tooltip(comment) if comment
|
|
532
|
+
cell
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def invalidate_rendered_cells(sheet, cells)
|
|
536
|
+
if cells.nil?
|
|
537
|
+
@rendered_cells.delete_if { |key, _cell| sheet.nil? || key[0] == sheet }
|
|
538
|
+
return
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
if cells.is_a?(Workbook::CellRange)
|
|
542
|
+
@rendered_cells.delete_if do |key, _cell|
|
|
543
|
+
key[0] == sheet && cells.include?(key[1], key[2])
|
|
544
|
+
end
|
|
545
|
+
return
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
changed = cells.to_h { |row, column| [[sheet, row, column], true] }
|
|
549
|
+
@rendered_cells.delete_if { |key, _cell| changed.key?(key.first(3)) }
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def selection_changed(areas, cx)
|
|
553
|
+
area = areas.last
|
|
554
|
+
return unless area
|
|
555
|
+
row, column = [area.rows.begin, 1].max, [area.columns.begin, 1].max
|
|
556
|
+
return if row > Workbook::MAX_ROWS || column > Workbook::MAX_COLUMNS
|
|
557
|
+
|
|
558
|
+
if @editing_cell && @editing_cell != [row, column] && !commit_inline_edit
|
|
559
|
+
edited_row, edited_column = @editing_cell
|
|
560
|
+
@grid.selection = [UI::Grid::Area.new(rows: edited_row...(edited_row + 1),
|
|
561
|
+
columns: edited_column...(edited_column + 1))]
|
|
562
|
+
cx.window.request_frame if cx.respond_to?(:window)
|
|
563
|
+
return
|
|
564
|
+
end
|
|
565
|
+
@active_cell = [row, column]
|
|
566
|
+
sync_formula_field unless @editing_cell == @active_cell
|
|
567
|
+
update_status(area)
|
|
568
|
+
cx.window.request_frame if cx.respond_to?(:window)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def begin_edit(row, column, cx)
|
|
572
|
+
return if row.zero? || column.zero?
|
|
573
|
+
return if @editing_cell == [row, column]
|
|
574
|
+
return unless commit_inline_edit if @editing_cell && @editing_cell != [row, column]
|
|
575
|
+
|
|
576
|
+
@active_cell = [row, column]
|
|
577
|
+
sync_formula_field
|
|
578
|
+
@editing_cell = [row, column]
|
|
579
|
+
@inline_buffer = Zaniah::TextBuffer.new(@workbook.input_at(row, column).to_s.encode(Encoding::UTF_8))
|
|
580
|
+
@inline_editor = Zaniah::Text.new(@inline_buffer.to_s, size: 12, color: cx.theme.colors.text, wrap: :none)
|
|
581
|
+
.editable(@inline_buffer)
|
|
582
|
+
.style(width: percent(100), height: percent(100), padding: [0, 4])
|
|
583
|
+
.on_change do |text|
|
|
584
|
+
@formula_field.buffer.replace(0...@formula_field.buffer.bytesize, text.encode(Encoding::UTF_8))
|
|
585
|
+
update_completion(text)
|
|
586
|
+
cx.window.request_frame
|
|
587
|
+
end
|
|
588
|
+
@inline_editor.selection = Zaniah::TextSelection.new(@inline_buffer.bytesize)
|
|
589
|
+
@inline_focus_pending = true
|
|
590
|
+
@rendered_cells.delete_if { |key, _cell| key[0] == @workbook.active_sheet && key[1] == row && key[2] == column }
|
|
591
|
+
cx.dispatcher.focus(@formula_field.focus_handle) if @formula_field.focus_handle
|
|
592
|
+
request_frame
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def commit_inline_edit
|
|
596
|
+
return false unless @editing_cell
|
|
597
|
+
|
|
598
|
+
row, column = @editing_cell
|
|
599
|
+
value = CSVFile.parse_cell(@inline_buffer.to_s)
|
|
600
|
+
value.nil? ? @workbook.clear(row, column) : @workbook.set(row, column, value)
|
|
601
|
+
@editing_cell = @inline_buffer = @inline_editor = nil
|
|
602
|
+
@inline_focus_pending = false
|
|
603
|
+
@status = "Applied #{cell_address(row, column)}"
|
|
604
|
+
@rendered_cells.delete_if { |key, _cell| key[0] == @workbook.active_sheet && key[1] == row && key[2] == column }
|
|
605
|
+
sync_formula_field
|
|
606
|
+
@cx&.dispatcher&.focus(@grid.focus_handle)
|
|
607
|
+
request_frame
|
|
608
|
+
true
|
|
609
|
+
rescue Rukbat::Error, ArgumentError, TypeError => error
|
|
610
|
+
@status = error.message
|
|
611
|
+
request_frame
|
|
612
|
+
false
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def cancel_inline_edit
|
|
616
|
+
return false unless @editing_cell
|
|
617
|
+
|
|
618
|
+
row, column = @editing_cell
|
|
619
|
+
@editing_cell = @inline_buffer = @inline_editor = nil
|
|
620
|
+
@inline_focus_pending = false
|
|
621
|
+
@rendered_cells.delete_if { |key, _cell| key[0] == @workbook.active_sheet && key[1] == row && key[2] == column }
|
|
622
|
+
sync_formula_field
|
|
623
|
+
@cx&.dispatcher&.focus(@grid.focus_handle)
|
|
624
|
+
request_frame
|
|
625
|
+
true
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def fill(source, target)
|
|
629
|
+
source_top, source_left = [source.rows.begin, 1].max, [source.columns.begin, 1].max
|
|
630
|
+
source_bottom, source_right = source.rows.end, source.columns.end
|
|
631
|
+
return if source_top >= source_bottom || source_left >= source_right
|
|
632
|
+
|
|
633
|
+
target_top, target_left = [target.rows.begin, 1].max, [target.columns.begin, 1].max
|
|
634
|
+
fill_cells = (target.rows.end - target_top) * (target.columns.end - target_left)
|
|
635
|
+
if fill_cells > MAX_FILL_CELLS
|
|
636
|
+
@status = "Fill area exceeds #{MAX_FILL_CELLS} cells"
|
|
637
|
+
request_frame
|
|
638
|
+
return false
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
source_height, source_width = source_bottom - source_top, source_right - source_left
|
|
642
|
+
changes = []
|
|
643
|
+
(target_top...target.rows.end).each do |row|
|
|
644
|
+
(target_left...target.columns.end).each do |column|
|
|
645
|
+
next if row >= source_top && row < source_bottom && column >= source_left && column < source_right
|
|
646
|
+
|
|
647
|
+
from_row = source_top + ((row - source_top) % source_height)
|
|
648
|
+
from_column = source_left + ((column - source_left) % source_width)
|
|
649
|
+
value = @workbook.input_at(from_row, from_column)
|
|
650
|
+
value = translated_formula(value, from_row, from_column, row, column) if value.is_a?(String) && value.start_with?("=")
|
|
651
|
+
changes << [row, column, value]
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
@workbook.set_many(changes) unless changes.empty?
|
|
655
|
+
update_status(target)
|
|
656
|
+
request_frame
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def translated_formula(value, from_row, from_column, to_row, to_column)
|
|
660
|
+
from = Furud::Reference.new(sheet: @workbook.active_sheet, row: from_row, column: from_column)
|
|
661
|
+
to = Furud::Reference.new(sheet: @workbook.active_sheet, row: to_row, column: to_column)
|
|
662
|
+
@workbook.translate_formula(value, from: from, to: to)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def update_status(area = @grid.selection.last)
|
|
666
|
+
return @status = "Ready" unless area
|
|
667
|
+
|
|
668
|
+
top, left = [area.rows.begin, 1].max, [area.columns.begin, 1].max
|
|
669
|
+
bottom, right = [area.rows.end - 1, top].max, [area.columns.end - 1, left].max
|
|
670
|
+
summary = @workbook.summary(top, left, bottom, right)
|
|
671
|
+
@status = "#{summary.count} cells · Sum #{summary.sum} · Average #{summary.average || "—"} · Numeric #{summary.numeric_count}"
|
|
672
|
+
rescue Rukbat::Error => error
|
|
673
|
+
@status = error.message
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def sync_formula_field
|
|
677
|
+
value = @workbook.input_at(*@active_cell).to_s
|
|
678
|
+
@formula_field.buffer.replace(0...@formula_field.buffer.bytesize, value.encode(Encoding::UTF_8))
|
|
679
|
+
comment = @workbook.comment_at(*@active_cell).to_s
|
|
680
|
+
@comment_field.buffer.replace(0...@comment_field.buffer.bytesize, comment.encode(Encoding::UTF_8))
|
|
681
|
+
update_completion(value)
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def update_completion(text)
|
|
685
|
+
token = text[/([A-Za-z][A-Za-z0-9_.]*)\z/, 1]
|
|
686
|
+
@completion_candidate = nil
|
|
687
|
+
return unless token && (text.start_with?("=") || text.match?(/[+\-*\/(,]\s*#{Regexp.escape(token)}\z/i))
|
|
688
|
+
return if token.length < 2
|
|
689
|
+
|
|
690
|
+
@completion_session.query = token
|
|
691
|
+
match = @completion_session.matches(1).first
|
|
692
|
+
@completion_candidate = match.candidate if match && match.candidate != token.upcase
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def apply_completion
|
|
696
|
+
return unless @completion_candidate
|
|
697
|
+
token = @formula_field.value[/([A-Za-z][A-Za-z0-9_.]*)\z/, 1]
|
|
698
|
+
return unless token
|
|
699
|
+
|
|
700
|
+
text = @formula_field.value
|
|
701
|
+
value = text.byteslice(0, text.bytesize - token.bytesize) + @completion_candidate
|
|
702
|
+
@formula_field.buffer.replace(0...@formula_field.buffer.bytesize, value.encode(Encoding::UTF_8))
|
|
703
|
+
update_completion(value)
|
|
704
|
+
request_frame
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def apply_format(**properties)
|
|
708
|
+
coordinates = selected_coordinates
|
|
709
|
+
@workbook.format_range(*coordinates, sheet: @workbook.active_sheet, **properties)
|
|
710
|
+
@status = "Formatted #{cell_address(coordinates[0], coordinates[1])}"
|
|
711
|
+
request_frame
|
|
712
|
+
true
|
|
713
|
+
rescue Rukbat::Error => error
|
|
714
|
+
@status = error.message
|
|
715
|
+
request_frame
|
|
716
|
+
false
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
def toggle_bold
|
|
720
|
+
value = !@workbook.format_at(*@active_cell).fetch(:bold, false)
|
|
721
|
+
apply_format(bold: value)
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def freeze_panes
|
|
725
|
+
rows, columns = @active_cell[0] + 1, @active_cell[1] + 1
|
|
726
|
+
@workbook.set_frozen_panes(rows: rows, columns: columns)
|
|
727
|
+
@grid.freeze_panes(rows: rows, columns: columns)
|
|
728
|
+
@status = "Frozen through #{cell_address(*@active_cell)}"
|
|
729
|
+
request_frame
|
|
730
|
+
true
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
def unfreeze_panes
|
|
734
|
+
@workbook.set_frozen_panes(rows: 0, columns: 0)
|
|
735
|
+
@grid.freeze_panes(rows: 0, columns: 0)
|
|
736
|
+
@status = "Unfrozen panes"
|
|
737
|
+
request_frame
|
|
738
|
+
true
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def sync_frozen_panes
|
|
742
|
+
rows, columns = @workbook.frozen_panes
|
|
743
|
+
@grid.freeze_panes(rows: rows, columns: columns)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def show_chart(type)
|
|
747
|
+
top, left, bottom, right = selected_coordinates
|
|
748
|
+
if bottom == top
|
|
749
|
+
@status = "Select a header and at least one data row"
|
|
750
|
+
request_frame
|
|
751
|
+
return false
|
|
752
|
+
end
|
|
753
|
+
series = chart_series(top, left, bottom, right)
|
|
754
|
+
@chart_component = case type
|
|
755
|
+
when :line then UI::LineChart.new(series, width: 480, height: 180)
|
|
756
|
+
when :bar then UI::BarChart.new(series, width: 480, height: 180)
|
|
757
|
+
when :pie then UI::PieChart.new(pie_values(top, left, bottom, right), width: 320, height: 180)
|
|
758
|
+
else raise ArgumentError, "unsupported chart type"
|
|
759
|
+
end
|
|
760
|
+
@status = "#{type.to_s.capitalize} chart"
|
|
761
|
+
request_frame
|
|
762
|
+
true
|
|
763
|
+
rescue ArgumentError, Rukbat::Error => error
|
|
764
|
+
@status = error.message
|
|
765
|
+
request_frame
|
|
766
|
+
false
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def selected_coordinates
|
|
770
|
+
area = @grid.selection.last
|
|
771
|
+
return [*@active_cell, *@active_cell] unless area
|
|
772
|
+
|
|
773
|
+
top, left = [area.rows.begin, 1].max, [area.columns.begin, 1].max
|
|
774
|
+
bottom, right = [area.rows.end - 1, top].max, [area.columns.end - 1, left].max
|
|
775
|
+
[top, left, bottom, right]
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def chart_series(top, left, bottom, right)
|
|
779
|
+
columns = left == right ? [left] : ((left + 1)..right).to_a
|
|
780
|
+
raise ArgumentError, "select at least one numeric series column" if columns.empty?
|
|
781
|
+
|
|
782
|
+
columns.to_h do |column|
|
|
783
|
+
name = @workbook.input_at(top, column).to_s
|
|
784
|
+
name = "Series #{column_name(column)}" if name.empty?
|
|
785
|
+
values = ((top + 1)..bottom).map do |row|
|
|
786
|
+
value = @workbook[row, column]
|
|
787
|
+
value.nil? ? 0 : Float(value)
|
|
788
|
+
rescue ArgumentError, TypeError
|
|
789
|
+
0
|
|
790
|
+
end
|
|
791
|
+
[name, values]
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def pie_values(top, left, bottom, right)
|
|
796
|
+
column = right > left ? left + 1 : left
|
|
797
|
+
(top + 1..bottom).to_h do |row|
|
|
798
|
+
label = right > left ? @workbook[row, left].to_s : row.to_s
|
|
799
|
+
value = @workbook[row, column]
|
|
800
|
+
number = value.nil? ? 0 : Float(value)
|
|
801
|
+
[label.empty? ? row.to_s : label, number]
|
|
802
|
+
rescue ArgumentError, TypeError
|
|
803
|
+
[label.empty? ? row.to_s : label, 0]
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def add_sheet
|
|
808
|
+
index = @workbook.sheet_names.length + 1
|
|
809
|
+
name = "Sheet#{index}"
|
|
810
|
+
index += 1 while @workbook.sheet_names.include?(name)
|
|
811
|
+
@workbook.add_sheet(name)
|
|
812
|
+
sync_grid_visibility
|
|
813
|
+
sync_frozen_panes
|
|
814
|
+
@active_cell = [1, 1]
|
|
815
|
+
sync_formula_field
|
|
816
|
+
@status = "Added #{name}"
|
|
817
|
+
request_frame
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
def save
|
|
821
|
+
@status = @on_save ? @on_save.call.to_s : "Save is unavailable"
|
|
822
|
+
request_frame
|
|
823
|
+
true
|
|
824
|
+
rescue StandardError => error
|
|
825
|
+
@status = "Save failed: #{error.message}"
|
|
826
|
+
request_frame
|
|
827
|
+
false
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def request_frame = @cx&.window&.request_frame
|
|
831
|
+
|
|
832
|
+
def focused_text_field?(window)
|
|
833
|
+
window.dispatcher.focused&.ancestors&.any? { |handle| handle.context[:in_text_field] }
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def cell_address(row, column) = "#{column_name(column)}#{row}"
|
|
837
|
+
|
|
838
|
+
def column_name(column)
|
|
839
|
+
result = +""
|
|
840
|
+
while column.positive?
|
|
841
|
+
column, remainder = (column - 1).divmod(26)
|
|
842
|
+
result.prepend((65 + remainder).chr)
|
|
843
|
+
end
|
|
844
|
+
result
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
end
|