wineole 0.1.0 → 0.2.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 +4 -4
- data/bin/wineole-vba +45 -0
- data/lib/wineole/client.rb +454 -20
- data/lib/wineole/dispatcher.rb +534 -0
- data/lib/wineole/errors.rb +6 -0
- data/lib/wineole/events.rb +353 -0
- data/lib/wineole/msoffice/address.rb +67 -0
- data/lib/wineole/msoffice/book.rb +114 -0
- data/lib/wineole/msoffice/color.rb +87 -0
- data/lib/wineole/msoffice/controls.rb +450 -0
- data/lib/wineole/msoffice/excel.rb +311 -0
- data/lib/wineole/msoffice/format.rb +362 -0
- data/lib/wineole/msoffice/forms.rb +163 -0
- data/lib/wineole/msoffice/passthrough.rb +46 -0
- data/lib/wineole/msoffice/paths.rb +62 -0
- data/lib/wineole/msoffice/range.rb +149 -0
- data/lib/wineole/msoffice/sheet.rb +101 -0
- data/lib/wineole/msoffice/vba.rb +199 -0
- data/lib/wineole/msoffice/vba_api.rb +366 -0
- data/lib/wineole/msoffice/vba_block.rb +144 -0
- data/lib/wineole/msoffice.rb +28 -0
- data/lib/wineole/proxy.rb +126 -15
- data/lib/wineole.rb +10 -1
- data/wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- metadata +21 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
require_relative '../proxy'
|
|
2
|
+
require_relative '../errors'
|
|
3
|
+
require_relative 'passthrough'
|
|
4
|
+
require_relative 'vba_api'
|
|
5
|
+
require_relative 'controls'
|
|
6
|
+
|
|
7
|
+
module WineOLE
|
|
8
|
+
module MSOffice
|
|
9
|
+
# `book.forms`: the UserForms of a workbook. A UserForm is a VBComponent
|
|
10
|
+
# of type 3 with a Designer; the form itself exists only while VBA has
|
|
11
|
+
# it loaded, as its default instance. The wrapper reaches that instance
|
|
12
|
+
# through four generated procedures in its own module, because a
|
|
13
|
+
# UserForm's default instance has no COM name a client can ask for.
|
|
14
|
+
#
|
|
15
|
+
# Name note (M0): `forms` is free on Workbook.
|
|
16
|
+
class Forms
|
|
17
|
+
USERFORM_TYPE = 3
|
|
18
|
+
|
|
19
|
+
def initialize(book)
|
|
20
|
+
@book = book
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The name is checked here as well as in add_component, because it
|
|
24
|
+
# also becomes part of four procedure names.
|
|
25
|
+
def add(name)
|
|
26
|
+
Controls.check_name!(name)
|
|
27
|
+
component = @book.vba.add_component(name, kind: :form)
|
|
28
|
+
begin
|
|
29
|
+
Form.new(name, component, @book)
|
|
30
|
+
rescue StandardError
|
|
31
|
+
# Form.new's helper write failed after the component was already
|
|
32
|
+
# created; take the component back out rather than leaving it
|
|
33
|
+
# behind with no helper to show or unload it.
|
|
34
|
+
@book.vba.remove_component(name)
|
|
35
|
+
raise
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Re-bind an existing UserForm (one made earlier, or one in a workbook
|
|
40
|
+
# that was opened). nil for a missing name or a component that is not
|
|
41
|
+
# a UserForm. AccessDenied from `project` passes through.
|
|
42
|
+
#
|
|
43
|
+
# Only the lookup is rescued, deliberately: Form.new writes the helper
|
|
44
|
+
# block, and that write can be refused (a locked project, a refused
|
|
45
|
+
# AddFromString). Reporting a real failure as "not found" would send
|
|
46
|
+
# the caller looking for a form that is right there.
|
|
47
|
+
def [](name)
|
|
48
|
+
component = begin
|
|
49
|
+
found = @book.vba.project.VBComponents.Item(name)
|
|
50
|
+
found if found.Type == USERFORM_TYPE
|
|
51
|
+
rescue WineOLE::RemoteError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
return nil if component.nil?
|
|
55
|
+
|
|
56
|
+
Form.new(name, component, @book)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The helper block. `Show 0` is modeless, and it is the only form the
|
|
60
|
+
# wrapper offers: a modal Show blocks Excel's message loop, and with
|
|
61
|
+
# it the bridge -- measured, the bridge freezes until the form closes.
|
|
62
|
+
def self.helper(name)
|
|
63
|
+
<<~VBA
|
|
64
|
+
Function WineOLE_Form_#{name}() As Object
|
|
65
|
+
Set WineOLE_Form_#{name} = #{name}
|
|
66
|
+
End Function
|
|
67
|
+
Sub WineOLE_Show_#{name}()
|
|
68
|
+
#{name}.Show 0
|
|
69
|
+
End Sub
|
|
70
|
+
Sub WineOLE_Hide_#{name}()
|
|
71
|
+
#{name}.Hide
|
|
72
|
+
End Sub
|
|
73
|
+
Sub WineOLE_Unload_#{name}()
|
|
74
|
+
Unload #{name}
|
|
75
|
+
End Sub
|
|
76
|
+
VBA
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# One UserForm. `ole` is the Designer (the design-time form: Caption,
|
|
81
|
+
# Width, Height, and the Controls that `controls` wraps). `instance` is
|
|
82
|
+
# the loaded form -- the object that shows, hides and fires events.
|
|
83
|
+
#
|
|
84
|
+
# Name note (M0): `name`, `component`, `ole`, `instance`, `show`,
|
|
85
|
+
# `hide`, `unload` measured free on the Designer; `controls` is the
|
|
86
|
+
# deliberate shadow.
|
|
87
|
+
class Form
|
|
88
|
+
include Passthrough
|
|
89
|
+
|
|
90
|
+
attr_reader :name, :component
|
|
91
|
+
|
|
92
|
+
# Writing the helper on every construction (add and re-bind alike) is
|
|
93
|
+
# what makes a form in a reopened workbook showable without the
|
|
94
|
+
# caller remembering to do anything; write is an upsert.
|
|
95
|
+
def initialize(name, component, book)
|
|
96
|
+
@name = name
|
|
97
|
+
@component = component
|
|
98
|
+
@book = book
|
|
99
|
+
@ole = component.Designer
|
|
100
|
+
@runtime = {}
|
|
101
|
+
@instance = nil
|
|
102
|
+
@book.vba.write(Forms.helper(name), name: "form_#{name}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def controls
|
|
106
|
+
@controls ||= UserFormControls.new(self, @book.vba)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The default instance. Referencing it loads the form if it is not
|
|
110
|
+
# loaded (VBA auto-instantiation), so `shown?` before `show` answers
|
|
111
|
+
# false and leaves the form loaded but hidden. Cached until `unload`.
|
|
112
|
+
def instance
|
|
113
|
+
@instance ||= run('Form')
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The live counterpart of a design-time control, by name. Cached so
|
|
117
|
+
# that `on` and the `off` that undoes it meet the same Events.
|
|
118
|
+
def runtime_control(control_name)
|
|
119
|
+
@runtime[control_name] ||= instance.Controls.Item(control_name)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Modeless, always. Returns as soon as the form is on screen; the
|
|
123
|
+
# bridge stays responsive, which is what lets events reach Ruby.
|
|
124
|
+
def show
|
|
125
|
+
run('Show')
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Through VBA, like show and unload: the extender's Hide answers
|
|
130
|
+
# GetIDsOfNames but refuses every out-of-process Invoke (measured,
|
|
131
|
+
# DISP_E_MEMBERNOTFOUND whatever the flags), while Visible reads fine.
|
|
132
|
+
def hide
|
|
133
|
+
run('Hide')
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def shown?
|
|
138
|
+
instance.Visible ? true : false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Unloading destroys the runtime controls, and with them every event
|
|
142
|
+
# connection Ruby holds on them, so those are closed first -- on our
|
|
143
|
+
# side, deliberately, rather than left to fail when the object is
|
|
144
|
+
# gone. The next `instance` loads a fresh form.
|
|
145
|
+
def unload
|
|
146
|
+
@runtime.each_value { |control| control.ole_events.close }
|
|
147
|
+
@instance&.ole_events&.close
|
|
148
|
+
@runtime.clear
|
|
149
|
+
@instance = nil
|
|
150
|
+
run('Unload')
|
|
151
|
+
self
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Qualified with the workbook name so the right book's procedure runs
|
|
157
|
+
# when several are open (M2 measured this form working on Excel 11).
|
|
158
|
+
def run(verb)
|
|
159
|
+
@book.ole.Application.Run("'#{@book.ole.Name}'!WineOLE_#{verb}_#{@name}")
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative '../proxy'
|
|
2
|
+
|
|
3
|
+
module WineOLE
|
|
4
|
+
module MSOffice
|
|
5
|
+
# Shared by every wrapper around a COM object that otherwise passes
|
|
6
|
+
# unknown methods straight through to it: the `ole` reader, plus
|
|
7
|
+
# `method_missing`/`respond_to_missing?`. `Range`, `Sheet`, `Book`,
|
|
8
|
+
# `Control` and `Form` each need exactly these three members; `Control`
|
|
9
|
+
# overrides `passthrough_target`; extracted here so they are written
|
|
10
|
+
# once instead of copied three times.
|
|
11
|
+
#
|
|
12
|
+
# Deliberately does not define `initialize` -- the including classes
|
|
13
|
+
# take different constructor arguments (`Range.new(proxy)` vs.
|
|
14
|
+
# `Sheet.new(proxy, version:)` vs. `Book.new(proxy, client:, version:,
|
|
15
|
+
# convert_paths:)`), so each sets `@ole` itself.
|
|
16
|
+
module Passthrough
|
|
17
|
+
# The underlying Proxy, for reaching COM explicitly.
|
|
18
|
+
attr_reader :ole
|
|
19
|
+
|
|
20
|
+
def method_missing(name, *args, &block)
|
|
21
|
+
return super if Proxy::IMPLICIT_CONVERSIONS.include?(name)
|
|
22
|
+
|
|
23
|
+
passthrough_target.public_send(name, *args, &block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond_to_missing?(name, include_private = false)
|
|
27
|
+
# Anything not named here is a COM member as far as this class is
|
|
28
|
+
# concerned -- the same stance Proxy takes, and for the same reason:
|
|
29
|
+
# Ruby probes to_ary, to_str, coerce and friends behind the scenes,
|
|
30
|
+
# and answering `true` to those makes e.g. `puts obj` try a
|
|
31
|
+
# conversion that ends in NoMethodError from inside the interpreter.
|
|
32
|
+
# Reuse Proxy's list rather than keeping a second copy of it.
|
|
33
|
+
!Proxy::IMPLICIT_CONVERSIONS.include?(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Where unknown methods go. `@ole` for every wrapper except `Control`,
|
|
39
|
+
# which forwards to the object that has Caption and Value rather than
|
|
40
|
+
# to the OLEObject host around it (see Control's class comment).
|
|
41
|
+
def passthrough_target
|
|
42
|
+
@ole
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'English'
|
|
2
|
+
require_relative '../client'
|
|
3
|
+
|
|
4
|
+
module WineOLE
|
|
5
|
+
module MSOffice
|
|
6
|
+
# Linux <-> Wine path conversion, and the question of whether converting
|
|
7
|
+
# is meaningful at all.
|
|
8
|
+
#
|
|
9
|
+
# It is meaningful only when the client and the bridge are on one machine
|
|
10
|
+
# looking at one Wine prefix. Convert when that is not true and you get a
|
|
11
|
+
# path that silently refers to some other machine's filesystem.
|
|
12
|
+
module Paths
|
|
13
|
+
# Z:\..., C:/..., \\server\share\...
|
|
14
|
+
WINDOWS_SHAPED = %r{\A(?:[A-Za-z]:[\\/]|\\\\)}.freeze
|
|
15
|
+
|
|
16
|
+
# Deliberately keyed off the same loopback test the bridge uses to
|
|
17
|
+
# decide whether a token is required. If the two definitions of "local"
|
|
18
|
+
# drifted apart, a connection could be remote for authentication and
|
|
19
|
+
# local for paths at once.
|
|
20
|
+
#
|
|
21
|
+
# A host's own NIC address counts as remote. Enumerating local
|
|
22
|
+
# interfaces to notice otherwise would be more code, more edge cases
|
|
23
|
+
# (containers, NAT, temporary IPv6 addresses), and would reintroduce
|
|
24
|
+
# exactly that split.
|
|
25
|
+
def self.convertible?(client:, windows: Client::WINDOWS)
|
|
26
|
+
return false if windows # already Windows paths, and no winepath here
|
|
27
|
+
|
|
28
|
+
client.loopback?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Linux path -> Wine path. Returns the argument unchanged when it
|
|
32
|
+
# already looks like a Windows path, and when winepath is unavailable
|
|
33
|
+
# or fails.
|
|
34
|
+
#
|
|
35
|
+
# Failing to convert is not fatal -- the caller can write a Windows
|
|
36
|
+
# path themselves, and will see that they need to. Raising here would
|
|
37
|
+
# turn a recoverable inconvenience into a stopped script.
|
|
38
|
+
def self.to_wine(path)
|
|
39
|
+
return path if path.to_s.match?(WINDOWS_SHAPED)
|
|
40
|
+
|
|
41
|
+
run_winepath('-w', path) || path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Wine path -> Linux path. Same failure stance.
|
|
45
|
+
def self.to_local(path)
|
|
46
|
+
return path unless path.to_s.match?(WINDOWS_SHAPED)
|
|
47
|
+
|
|
48
|
+
run_winepath('-u', path) || path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.run_winepath(flag, path)
|
|
52
|
+
out = IO.popen(['winepath', flag, path.to_s], err: File::NULL, &:read)
|
|
53
|
+
return nil unless $CHILD_STATUS&.success?
|
|
54
|
+
|
|
55
|
+
out.strip.empty? ? nil : out.strip
|
|
56
|
+
rescue SystemCallError, IOError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
private_class_method :run_winepath
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require_relative '../proxy'
|
|
2
|
+
require_relative 'passthrough'
|
|
3
|
+
require_relative 'format'
|
|
4
|
+
|
|
5
|
+
module WineOLE
|
|
6
|
+
module MSOffice
|
|
7
|
+
# Wraps a COM Range. Adds exactly four methods plus `ole`; everything
|
|
8
|
+
# else falls through to COM.
|
|
9
|
+
#
|
|
10
|
+
# The restraint is deliberate. COM's Range already exposes Rows, Columns,
|
|
11
|
+
# Cells, Item, Areas, Find, Sort, Merge, Table and many more, and COM
|
|
12
|
+
# resolves names case-insensitively -- so every lowercase single word this
|
|
13
|
+
# class defines covers a COM call that already worked. `to_a`, `write`,
|
|
14
|
+
# `fill`, `format` and `ole` were each checked against a live Range and
|
|
15
|
+
# found absent.
|
|
16
|
+
class Range
|
|
17
|
+
include Passthrough
|
|
18
|
+
|
|
19
|
+
def initialize(proxy)
|
|
20
|
+
@ole = proxy
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Always a two-dimensional Array, whatever the range's size.
|
|
24
|
+
#
|
|
25
|
+
# Excel's own `Value` returns a bare scalar for a one-cell range --
|
|
26
|
+
# consistently, whether it was addressed as "A1" or "A1:A1" -- so
|
|
27
|
+
# generic code that does not know the size has to branch. This is that
|
|
28
|
+
# branch, written once.
|
|
29
|
+
#
|
|
30
|
+
# Returns a plain Array on purpose: Ruby's own vocabulary then covers
|
|
31
|
+
# the rest (`rows.transpose` for columns, `rows.flatten` for values),
|
|
32
|
+
# so this class does not have to grow `each_row`, `each_column` or
|
|
33
|
+
# `flatten` and risk shadowing more COM members.
|
|
34
|
+
def to_a
|
|
35
|
+
v = @ole.Value
|
|
36
|
+
v.is_a?(::Array) ? v : [[v]]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Write, refusing anything that does not fit.
|
|
40
|
+
#
|
|
41
|
+
# Excel's own assignment corrupts silently in three ways (all measured):
|
|
42
|
+
# a flat array written to a column replicates its first element down
|
|
43
|
+
# every cell, too few values leave #N/A behind, and too many are
|
|
44
|
+
# truncated. None of those raise, and none are visible without looking
|
|
45
|
+
# at the sheet.
|
|
46
|
+
def write(value)
|
|
47
|
+
@ole.Value = shaped(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Write, adapting the value to the range: replicate along a dimension
|
|
51
|
+
# the argument does not have, truncate or pad along one it does.
|
|
52
|
+
#
|
|
53
|
+
# Total -- every input has a defined result, no exceptions -- but note
|
|
54
|
+
# that it reproduces Excel's own column trap by construction: a flat
|
|
55
|
+
# array is a row, so filling an Nx1 column with [1,2,3] puts 1 in every
|
|
56
|
+
# cell. That is why `write` and not `fill` is what `sheet[addr] = x`
|
|
57
|
+
# uses; reach for this one deliberately.
|
|
58
|
+
def fill(value)
|
|
59
|
+
nrows = row_count
|
|
60
|
+
ncols = column_count
|
|
61
|
+
@ole.Value =
|
|
62
|
+
case value
|
|
63
|
+
when ::Array
|
|
64
|
+
if value.first.is_a?(::Array)
|
|
65
|
+
(0...nrows).map { |r| row = value[r] || []; (0...ncols).map { |c| row[c] } }
|
|
66
|
+
else
|
|
67
|
+
one = (0...ncols).map { |c| value[c] }
|
|
68
|
+
::Array.new(nrows) { one.dup }
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
::Array.new(nrows) { ::Array.new(ncols) { value } }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Apply formatting. Keys are documented on WineOLE::MSOffice::Format.
|
|
76
|
+
#
|
|
77
|
+
# An absent key leaves that attribute alone; `false` turns it off.
|
|
78
|
+
# That third state is why this takes keyword arguments rather than
|
|
79
|
+
# being a chain of verbs -- and it keeps this class's additions to one
|
|
80
|
+
# name, which matters because COM resolves names case-insensitively
|
|
81
|
+
# and every lowercase word here covers a COM member of the same
|
|
82
|
+
# spelling. `format` was measured free on a live Range.
|
|
83
|
+
#
|
|
84
|
+
# Note for anyone editing this class: defining `format` shadows
|
|
85
|
+
# Kernel#format inside these instance methods, so string formatting
|
|
86
|
+
# here must be written as `::Kernel.format(...)`.
|
|
87
|
+
def format(**opts)
|
|
88
|
+
Format.apply(@ole, opts)
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def row_count
|
|
95
|
+
@ole.Rows.Count
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def column_count
|
|
99
|
+
@ole.Columns.Count
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def shaped(value)
|
|
103
|
+
return value unless value.is_a?(::Array)
|
|
104
|
+
|
|
105
|
+
nrows = row_count
|
|
106
|
+
ncols = column_count
|
|
107
|
+
|
|
108
|
+
if value.first.is_a?(::Array)
|
|
109
|
+
unless value.all? { |r| r.is_a?(::Array) }
|
|
110
|
+
raise ArgumentError,
|
|
111
|
+
"range is #{nrows}x#{ncols}, but the value mixes rows and scalars"
|
|
112
|
+
end
|
|
113
|
+
widths = value.map(&:length).uniq
|
|
114
|
+
if widths.length > 1
|
|
115
|
+
raise ArgumentError,
|
|
116
|
+
"range is #{nrows}x#{ncols}, but the value has ragged rows " \
|
|
117
|
+
"(#{widths.join(', ')} elements)"
|
|
118
|
+
end
|
|
119
|
+
if value.any? { |r| r.any? { |c| c.is_a?(::Array) } }
|
|
120
|
+
raise ArgumentError,
|
|
121
|
+
"range is #{nrows}x#{ncols}, but the value nests more than two deep"
|
|
122
|
+
end
|
|
123
|
+
unless value.length == nrows && widths.first == ncols
|
|
124
|
+
raise ArgumentError,
|
|
125
|
+
"range is #{nrows}x#{ncols}, but the value is " \
|
|
126
|
+
"#{value.length}x#{widths.first}"
|
|
127
|
+
end
|
|
128
|
+
value
|
|
129
|
+
else
|
|
130
|
+
if value.any? { |v| v.is_a?(::Array) } # rubocop:disable Style/IfInsideElse
|
|
131
|
+
raise ArgumentError,
|
|
132
|
+
"range is #{nrows}x#{ncols}, but the value mixes scalars and rows"
|
|
133
|
+
end
|
|
134
|
+
if nrows > 1 && ncols > 1
|
|
135
|
+
raise ArgumentError,
|
|
136
|
+
"range is #{nrows}x#{ncols}; a flat array only fits a single " \
|
|
137
|
+
'row or column -- pass rows, or use fill'
|
|
138
|
+
end
|
|
139
|
+
expected = nrows > 1 ? nrows : ncols
|
|
140
|
+
unless value.length == expected
|
|
141
|
+
raise ArgumentError,
|
|
142
|
+
"range is #{nrows}x#{ncols}, but the value has #{value.length} elements"
|
|
143
|
+
end
|
|
144
|
+
nrows > 1 ? value.map { |v| [v] } : [value]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
require_relative '../proxy'
|
|
2
|
+
require_relative 'passthrough'
|
|
3
|
+
require_relative 'address'
|
|
4
|
+
require_relative 'range'
|
|
5
|
+
require_relative 'vba_api'
|
|
6
|
+
require_relative 'controls'
|
|
7
|
+
|
|
8
|
+
module WineOLE
|
|
9
|
+
module MSOffice
|
|
10
|
+
# Wraps a COM Worksheet. `[]`/`[]=` address it through the same `Address`
|
|
11
|
+
# parser the whole wrapper uses, so `sheet['A1:B2']` and
|
|
12
|
+
# `sheet[row, col]` both hand back a `Range`; everything else falls
|
|
13
|
+
# through to COM.
|
|
14
|
+
class Sheet
|
|
15
|
+
include Passthrough
|
|
16
|
+
|
|
17
|
+
def initialize(proxy, version:)
|
|
18
|
+
@ole = proxy
|
|
19
|
+
@version = version
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def [](*keys)
|
|
23
|
+
range_for(keys)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Delegates to Range#write, never #fill: write raises on a value that
|
|
27
|
+
# does not fit the range rather than replicating or padding it, which
|
|
28
|
+
# is the behaviour an assignment through []= should have.
|
|
29
|
+
def []=(*args)
|
|
30
|
+
value = args.pop
|
|
31
|
+
range_for(args).write(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# This worksheet's VBA surface. Blocks only -- a worksheet's module
|
|
35
|
+
# cannot be created or deleted, so SheetVBA has no component methods
|
|
36
|
+
# rather than having ones that always fail.
|
|
37
|
+
def vba
|
|
38
|
+
@vba ||= SheetVBA.new(@ole)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The Forms-toolbar controls on this sheet: `form_controls.add(:button,
|
|
42
|
+
# name: 'Go', at: 'B2')`. See FormControls for what they can and
|
|
43
|
+
# cannot do; `activex` is the family whose events reach Ruby.
|
|
44
|
+
#
|
|
45
|
+
# Name note (M0, measured against Excel 11): `form_controls` and
|
|
46
|
+
# `activex` are both free on Worksheet.
|
|
47
|
+
def form_controls
|
|
48
|
+
@form_controls ||= FormControls.new(self)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The ActiveX controls on this sheet, MSForms or any registered
|
|
52
|
+
# ProgID: `activex.add(:command_button, name: 'Go', at: 'B2')`.
|
|
53
|
+
def activex
|
|
54
|
+
@activex ||= ActiveXControls.new(self)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def range_for(keys)
|
|
60
|
+
return Range.new(@ole.Cells(*keys)) if cell_reference?(keys)
|
|
61
|
+
|
|
62
|
+
unless keys.length == 1 && keys.first.is_a?(::String)
|
|
63
|
+
raise ArgumentError, "unsupported sheet index #{keys.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
str = keys.first
|
|
67
|
+
addr = Address.parse(str, @version)
|
|
68
|
+
|
|
69
|
+
# An address that parses but stops at a sheet or a book (or does
|
|
70
|
+
# not parse at all) is a lookup, not something with cells to read
|
|
71
|
+
# or write -- see the spec on why `xl["Sheet1!"] = 0` filling a
|
|
72
|
+
# whole sheet is a hazard rather than a convenience (Spec §4.5).
|
|
73
|
+
if addr.nil? || !addr.range?
|
|
74
|
+
raise ArgumentError, "#{str.inspect} has no range"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# This object is one sheet. An address that names a different
|
|
78
|
+
# workbook or worksheet would silently reach past it -- writing to
|
|
79
|
+
# the sheet the caller *named* instead of the one they *have* is
|
|
80
|
+
# exactly the class of silent wrong-target write this wrapper
|
|
81
|
+
# exists to prevent.
|
|
82
|
+
if named?(addr.workbook) || named?(addr.worksheet)
|
|
83
|
+
raise ArgumentError,
|
|
84
|
+
"#{str.inspect} names another workbook or worksheet; a Sheet " \
|
|
85
|
+
'addresses its own cells only -- reach another sheet through ' \
|
|
86
|
+
'the Excel object that owns it'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Range.new(@ole.Range(addr.range))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def cell_reference?(keys)
|
|
93
|
+
keys.length == 2 && keys.all? { |k| k.is_a?(::Integer) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def named?(part)
|
|
97
|
+
!part.nil? && !part.empty?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|