osaka 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -6,9 +6,20 @@ This is a Ruby 1.9 library for automating work via GUI on Mac (using OSA/Applesc
6
6
 
7
7
  $ sudo gem install osaka
8
8
 
9
+ = Note before use
10
+
11
+ You have to enable the "access for assistive devices" for the scripts to run. You do this via System Preferences -> Universal Access / Accessibility -> "enable access for assistive devices"
12
+
9
13
  = How to use
10
14
 
11
15
  Examples:
16
+
17
+ #### Example: Using Osaka Flows to merge multiple keynote files.
18
+
19
+ require 'osaka'
20
+
21
+ # Convert all keynote files in dir "dirname" with the search pattern "pattern" and write it in "results.key"
22
+ CommonFlows.keynote_combine_files_from_directory_sorted("results.key", "dirname", /^\d+.*\.key$/)
12
23
 
13
24
  #### Example: Controlling Keynote
14
25
 
@@ -46,24 +57,37 @@ Examples:
46
57
  app.save
47
58
  app.quit
48
59
 
49
- #### Example: Using the Application Wrapper (lower-level)
60
+ #### Example: Using the lower-level remote control
50
61
 
51
62
  require 'osaka'
52
63
 
53
64
  # Create an application wrapper
54
- calculator = Osaka::ApplicationWrapper.new("Calculator")
65
+ calculator = Osaka::RemoteControl.new("Calculator")
55
66
 
56
67
  # Start the application
57
68
  calculator.activate
69
+ calculator.focus
58
70
 
59
71
  # Control the calculator
60
- calculator.click('button "1" of group 2 of window "Calculator"')
61
- calculator.click('button "+" of group 2 of window "Calculator"')
62
- calculator.click('button "4" of group 2 of window "Calculator"')
63
- calculator.click('button "=" of group 2 of window "Calculator"')
72
+ calculator.click!(at.button("1").group(2))
73
+ calculator.click!(at.button("+").group(2))
74
+ calculator.click!(at.button("4").group(2))
75
+ calculator.click!(at.button("=").group(2))
64
76
 
65
77
  calculator.quit
66
78
 
79
+ #### Example: Same as above using the Calculator class
80
+
81
+ require 'osaka'
82
+ calculator = Osaka::Calculator.new
83
+ calculator.activate
84
+ calculator.click("1")
85
+ calculator.click("+")
86
+ calculator.click("4")
87
+ calculator.click("=")
88
+ calculator.quit
89
+
90
+
67
91
  API document: To be done.
68
92
 
69
93
  = Source code
@@ -72,9 +96,9 @@ https://github.com/basvodde/osaka
72
96
 
73
97
  The license of this source is "BSD Licence"
74
98
 
75
- = Note on current version (0.1.1)
99
+ = Note on current version (0.4.0)
76
100
 
77
- Currently the functionality is fairly limited. It can be easily expanded by writing more application wrappers with convenient interfaces for controlling specific application. If you write some for common application or extend the ones, please submit a merge request :)
101
+ Current version is reasonably well tested on snow leopard, lion and mountain lion. It has been used to automate keynote, pages, and numbers work. Additional functionality would be useful but hasn't been needed yet. If you do need additional functionality, please extend it and send me a pull request :)
78
102
 
79
103
  = Supported environments
80
104
 
@@ -1,33 +1,34 @@
1
1
 
2
2
  module Osaka
3
3
  class Calculator < TypicalApplication
4
- attr_accessor :wrapper
4
+
5
+ attr_accessor :control
5
6
 
6
7
  def initialize
7
8
  @name = "Calculator"
8
- @wrapper = ApplicationWrapper.new("Calculator")
9
- @wrapper.set_current_window(@name)
9
+ @control = RemoteControl.new("Calculator")
10
+ control.set_current_window(@name)
10
11
  end
11
12
 
12
13
  def activate
13
14
  super
14
- if (@wrapper.current_window_name.empty?)
15
+ if (control.current_window_name.empty?)
15
16
  wait_for_new_window([])
16
- @wrapper.set_current_window(@wrapper.window_list[0])
17
+ control.set_current_window(control.window_list[0])
17
18
  end
18
19
  end
19
20
 
20
21
  def click(key)
21
- @wrapper.click!(at.button(key).group(2))
22
+ control.click!(at.button(key).group(2))
22
23
  end
23
24
 
24
25
  def key(k)
25
- @wrapper.keystroke(k)
26
+ control.keystroke(k)
26
27
  end
27
28
 
28
29
  def result
29
- @wrapper.wait_until_exists!(at.static_text(1).group(1))
30
- @wrapper.get!('value', at.static_text(1).group(1))
30
+ control.wait_until_exists!(at.static_text(1).group(1))
31
+ control.get!('value', at.static_text(1).group(1))
31
32
  end
32
33
  end
33
34
  end
data/lib/osaka/keynote.rb CHANGED
@@ -12,14 +12,14 @@ module Osaka
12
12
  end
13
13
 
14
14
  def create_print_dialog(location)
15
- KeynotePrintDialog.new(location, @wrapper)
15
+ KeynotePrintDialog.new(control.name, at.window("Print"))
16
16
  end
17
17
 
18
18
  def select_all_slides
19
- if @wrapper.exists(at.button("Slides").group(1).outline(1).scroll_area(2).splitter_group(1).splitter_group(1))
20
- @wrapper.click(at.button("Slides").group(1).outline(1).scroll_area(2).splitter_group(1).splitter_group(1))
19
+ if control.exists?(at.button("Slides").group(1).outline(1).scroll_area(2).splitter_group(1).splitter_group(1))
20
+ control.click(at.button("Slides").group(1).outline(1).scroll_area(2).splitter_group(1).splitter_group(1))
21
21
  else
22
- @wrapper.click(at.button("Slides").group(1).outline(1).scroll_area(1).splitter_group(1).splitter_group(1))
22
+ control.click(at.button("Slides").group(1).outline(1).scroll_area(1).splitter_group(1).splitter_group(1))
23
23
  end
24
24
  select_all
25
25
  end
@@ -17,6 +17,10 @@ module Osaka
17
17
  Location.new(to_s + " of " + other_location.to_s)
18
18
  end
19
19
 
20
+ def top_level_element
21
+ Location.new(@location_name[/window (.*)$/])
22
+ end
23
+
20
24
  def to_s
21
25
  @location_name
22
26
  end
data/lib/osaka/numbers.rb CHANGED
@@ -7,7 +7,7 @@ module Osaka
7
7
  end
8
8
 
9
9
  def fill_cell(column, row, value)
10
- @wrapper.tell("tell document 1; tell sheet 1; tell table 1; set value of cell #{column} of row #{row} to \"#{value}\"; end tell; end tell; end tell")
10
+ control.tell("tell document 1; tell sheet 1; tell table 1; set value of cell #{column} of row #{row} to \"#{value}\"; end tell; end tell; end tell")
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,132 @@
1
+
2
+ module Osaka
3
+ module OsakaExpectations
4
+
5
+ def simulate_mac_version(version)
6
+ control.should_receive(:mac_version).and_return(version)
7
+ end
8
+
9
+ def expect_execute_osascript(command = nil)
10
+ return Osaka::ScriptRunner.should_receive(:execute).with(command) unless command.nil?
11
+ Osaka::ScriptRunner.should_receive(:execute)
12
+ end
13
+
14
+ def expect_clone
15
+ control.should_receive(:clone)
16
+ end
17
+
18
+ def expect_activate
19
+ control.should_receive(:activate)
20
+ end
21
+
22
+ def expect_launch
23
+ control.should_receive(:launch)
24
+ end
25
+
26
+ def expect_focus
27
+ control.should_receive(:focus)
28
+ end
29
+
30
+ def expect_focus!
31
+ control.should_receive(:focus!)
32
+ end
33
+
34
+ def expect_set_current_window(name)
35
+ control.should_receive(:set_current_window).with(name)
36
+ end
37
+
38
+ def expect_running?
39
+ control.should_receive(:running?)
40
+ end
41
+
42
+ def expect_quit
43
+ control.should_receive(:quit)
44
+ end
45
+
46
+ def expect_window_list
47
+ control.should_receive(:window_list)
48
+ end
49
+
50
+ def expect_current_window_name
51
+ control.should_receive(:current_window_name)
52
+ end
53
+
54
+ def expect_set(element, location, value)
55
+ control.should_receive(:set).with(element, location, value)
56
+ end
57
+
58
+ def expect_set!(element, location, value)
59
+ control.should_receive(:set!).with(element, location, value)
60
+ end
61
+
62
+ def expect_get_app!(element)
63
+ control.should_receive(:get_app!).with(element)
64
+ end
65
+
66
+ def expect_keystroke(key, modifier = [])
67
+ control.should_receive(:keystroke).with(key, modifier).and_return(control) unless modifier.empty?
68
+ control.should_receive(:keystroke).with(key).and_return(control) if modifier.empty?
69
+ end
70
+
71
+ def expect_keystroke!(key, modifier = [])
72
+ control.should_receive(:keystroke!).with(key, modifier).and_return(control) unless modifier.empty?
73
+ control.should_receive(:keystroke!).with(key).and_return(control) if modifier.empty?
74
+ end
75
+
76
+ def expect_click!(location)
77
+ control.should_receive(:click!).with(location).and_return(control)
78
+ end
79
+
80
+ def expect_click(location)
81
+ control.should_receive(:click).with(location).and_return(control)
82
+ end
83
+
84
+ def expect_click_menu_bar(menu_item, menu_name)
85
+ control.should_receive(:click_menu_bar).with(menu_item, menu_name).and_return(control)
86
+ end
87
+
88
+ def expect_get!(element, location)
89
+ control.should_receive(:get!).with(element, location)
90
+ end
91
+
92
+ def expect_tell(do_this)
93
+ control.should_receive(:tell).with(do_this)
94
+ end
95
+
96
+ def expect_system_event(event)
97
+ control.should_receive(:system_event).with(event)
98
+ end
99
+
100
+ def expect_system_event!(event)
101
+ control.should_receive(:system_event!).with(event)
102
+ end
103
+
104
+ def expect_exists?(location)
105
+ control.should_receive(:exists?).with(location)
106
+ end
107
+
108
+ def expect_not_exists?(location)
109
+ control.should_receive(:not_exists?).with(location)
110
+ end
111
+
112
+ def expect_wait_until_exists(*location)
113
+ control.should_receive(:wait_until_exists).with(*location)
114
+ end
115
+
116
+ def expect_wait_until_exists!(*location)
117
+ control.should_receive(:wait_until_exists!).with(*location)
118
+ end
119
+
120
+ def expect_wait_until_not_exists(location)
121
+ control.should_receive(:wait_until_not_exists).with(location)
122
+ end
123
+
124
+ def expect_wait_until_not_exists!(location, action)
125
+ control.should_receive(:wait_until_not_exists!).with(location)
126
+ end
127
+
128
+ def expect_until_not_exists!(element)
129
+ control.should_receive(:until_not_exists!).with(element).and_yield
130
+ end
131
+ end
132
+ end
data/lib/osaka/pages.rb CHANGED
@@ -1,18 +1,18 @@
1
1
 
2
2
  module Osaka
3
3
  class PagesMailMergeDialog
4
- attr_accessor :wrapper, :location
4
+ attr_accessor :control, :location
5
5
 
6
- def initialize(location, wrapper)
6
+ def initialize(location, control)
7
7
  @location = location
8
- @wrapper = wrapper
8
+ @control = control
9
9
  end
10
10
 
11
11
  def merge
12
- @wrapper.click!(at.button("Merge").sheet(1))
13
- print_dialog_location = 'window "Print"'
14
- @wrapper.wait_until_exists!("menu button \"PDF\" of #{print_dialog_location}")
15
- TypicalPrintDialog.new(print_dialog_location, @wrapper)
12
+ control.click!(at.button("Merge").sheet(1))
13
+ print_dialog_location = at.window("Print")
14
+ control.wait_until_exists!(at.menu_button("PDF") + print_dialog_location)
15
+ TypicalPrintDialog.new(control.name, print_dialog_location)
16
16
  end
17
17
 
18
18
  def set_merge_to_new_document
@@ -25,9 +25,9 @@ module Osaka
25
25
 
26
26
  private
27
27
  def set_merge_to_document_printer(value)
28
- @wrapper.click(at.pop_up_button(2).sheet(1))
29
- @wrapper.wait_until_exists!(at.menu_item(value).menu(1).pop_up_button(2).sheet(1))
30
- @wrapper.click!(at.menu_item(value).menu(1).pop_up_button(2).sheet(1))
28
+ control.click(at.pop_up_button(2).sheet(1))
29
+ control.wait_until_exists!(at.menu_item(value).menu(1).pop_up_button(2).sheet(1))
30
+ control.click!(at.menu_item(value).menu(1).pop_up_button(2).sheet(1))
31
31
  end
32
32
  end
33
33
 
@@ -38,9 +38,9 @@ module Osaka
38
38
  end
39
39
 
40
40
  def mail_merge
41
- @wrapper.click_menu_bar(at.menu_item(20), "Edit")
42
- @wrapper.wait_until_exists(at.button("Merge").sheet(1))
43
- PagesMailMergeDialog.new(at.sheet(1), @wrapper)
41
+ control.click_menu_bar(at.menu_item(20), "Edit")
42
+ control.wait_until_exists(at.button("Merge").sheet(1))
43
+ PagesMailMergeDialog.new(at.sheet(1), control)
44
44
  end
45
45
 
46
46
  def mail_merge_to_pdf(filename)
@@ -1,20 +1,23 @@
1
1
 
2
+ require 'timeout'
3
+
2
4
  module Osaka
3
5
 
4
6
  class InvalidLocation < RuntimeError
5
7
  end
6
8
 
7
- class ApplicationWrapper
9
+ class RemoteControl
8
10
 
9
11
  attr_reader :name
12
+ attr_accessor :base_location
10
13
 
11
- def initialize(name)
14
+ def initialize(name, base_location = Location.new(""))
12
15
  @name = name
13
- @window = Location.new("")
16
+ @base_location = base_location
14
17
  end
15
18
 
16
19
  def ==(obj)
17
- @name == obj.name && current_window_name == obj.current_window_name
20
+ @name == obj.name && base_location == obj.base_location
18
21
  end
19
22
 
20
23
  def tell(command)
@@ -41,6 +44,10 @@ module Osaka
41
44
  def activate
42
45
  check_output( tell("activate"), "activate" )
43
46
  end
47
+
48
+ def launch
49
+ check_output( tell("launch"), "launch" )
50
+ end
44
51
 
45
52
  def quit
46
53
  keystroke("q", :command)
@@ -51,31 +58,38 @@ module Osaka
51
58
  system_event!(event)
52
59
  end
53
60
 
54
- def exists(location)
61
+ def exists?(location)
55
62
  system_event!("exists #{construct_location(location)}").strip == "true"
56
63
  end
57
64
 
58
- def not_exists(location)
65
+ def not_exists?(location)
59
66
  system_event!("not exists #{construct_location(location)}").strip == "true"
60
67
  end
61
68
 
62
69
  def wait_until(locations, action)
63
- while(true)
64
- locations.flatten.each { |location|
65
- return location if yield location
70
+
71
+ begin
72
+ Timeout::timeout(5) {
73
+ while(true)
74
+ locations.flatten.each { |location|
75
+ return location if yield location
76
+ }
77
+ action.() unless action.nil?
78
+ end
66
79
  }
67
- action.() unless action.nil?
80
+ rescue Exception => e
81
+ raise Osaka::TimeoutError, "Timed out while waiting for: #{locations.to_s}"
68
82
  end
69
83
  end
70
84
 
71
- def wait_until_exists(*locations)
85
+ def wait_until_exists(*locations, &action)
72
86
  activate
73
- wait_until_exists!(locations)
87
+ wait_until_exists!(*locations, &action)
74
88
  end
75
89
 
76
90
  def wait_until_exists!(*locations, &action)
77
91
  wait_until(locations, action) { |location|
78
- exists(location)
92
+ exists?(location)
79
93
  }
80
94
  end
81
95
 
@@ -84,12 +98,12 @@ module Osaka
84
98
 
85
99
  def wait_until_not_exists(*locations, &action)
86
100
  activate
87
- wait_until_not_exists!(*locations, action)
101
+ wait_until_not_exists!(*locations, &action)
88
102
  end
89
103
 
90
104
  def wait_until_not_exists!(*locations, &action)
91
105
  wait_until(locations, action) { |location|
92
- not_exists(location)
106
+ not_exists?(location)
93
107
  }
94
108
  end
95
109
 
@@ -116,7 +130,7 @@ module Osaka
116
130
  def keystroke(key, modifier_keys = [])
117
131
  activate
118
132
  focus
119
- keystroke!(key, modifier_keys)
133
+ (modifier_keys == []) ? keystroke!(key) : keystroke!(key, modifier_keys)
120
134
  end
121
135
 
122
136
  def click!(element)
@@ -141,21 +155,23 @@ module Osaka
141
155
  check_output( system_event!("set #{element}#{construct_prefixed_location(location)} to #{encoded_value}"), "set")
142
156
  end
143
157
 
144
- def focus
145
- current_windows = window_list
146
- currently_active_window = current_windows[0]
147
- currently_active_window ||= ""
148
-
149
- if current_window_invalid?(current_windows)
150
- @window = at.window(currently_active_window) unless currently_active_window.nil?
158
+ def focus
159
+ if (base_location.to_s.empty? || not_exists?(base_location))
160
+ currently_active_window = window_list[0]
161
+ currently_active_window ||= ""
162
+ @base_location = (currently_active_window.to_s.empty?) ? Location.new("") : at.window(currently_active_window)
151
163
  end
152
164
 
153
- set!("value", "attribute \"AXMain\"", true) unless currently_active_window == current_window_name
165
+ focus!
166
+ end
167
+
168
+ def focus!
169
+ system_event!("set value of attribute \"AXMain\" of #{base_location.top_level_element} to true") unless base_location.top_level_element.to_s.empty?
154
170
  end
155
171
 
156
172
  def form_location_with_window(location)
157
173
  new_location = Location.new(location)
158
- new_location += @window unless new_location.has_top_level_element?
174
+ new_location += @base_location unless new_location.has_top_level_element?
159
175
  new_location
160
176
  end
161
177
 
@@ -188,21 +204,42 @@ module Osaka
188
204
  end
189
205
 
190
206
  def set_current_window(window_name)
191
- @window = at.window(window_name)
207
+ @base_location = at.window(window_name)
192
208
  end
193
209
 
194
210
  def current_window_name
195
- matchdata = @window.to_s.match(/^window "(.*)"/)
211
+ matchdata = @base_location.to_s.match(/window "(.*)"/)
196
212
  return "" if matchdata.nil? || matchdata[1].nil?
197
213
  matchdata[1]
198
214
  end
199
215
 
200
- def current_window_location
201
- @window
216
+ def current_window_invalid?(window_list)
217
+ @base_location.to_s.empty? || window_list.index(current_window_name).nil?
218
+ end
219
+
220
+ def mac_version
221
+ @mac_version_string ||= Osaka::ScriptRunner.execute("system version of (system info)").strip
222
+ @mac_version ||= case @mac_version_string
223
+ when /^10.6.*/
224
+ :snow_leopard
225
+ when /^10.7.*/
226
+ :lion
227
+ when /^10.8.*/
228
+ :mountain_lion
229
+ else
230
+ :other
231
+ end
232
+
233
+ end
234
+
235
+ def mac_version_string
236
+ mac_version
237
+ @mac_version_string
202
238
  end
203
239
 
204
- def current_window_invalid?(window_list)
205
- @window.to_s.empty? || window_list.index(current_window_name).nil?
240
+
241
+ def convert_mac_version_string_to_symbol(version_string)
242
+
206
243
  end
207
244
 
208
245
  end
@@ -7,6 +7,9 @@ module Osaka
7
7
  class SystemCommandFailed < RuntimeError
8
8
  end
9
9
 
10
+ class TimeoutError < RuntimeError
11
+ end
12
+
10
13
  class VersioningError < RuntimeError
11
14
  end
12
15
 
@@ -14,9 +17,10 @@ module Osaka
14
17
 
15
18
  @@debug_info_enabled = false
16
19
 
17
- def self.enable_debug_prints(debug_info_format = :plain_text)
20
+ def self.enable_debug_prints(debug_info_format = :plain_text, filename = "")
18
21
  @@debug_info_enabled = true
19
22
  @@debug_info_format = debug_info_format
23
+ @@debug_info_script_filename = filename
20
24
  end
21
25
 
22
26
  def self.disable_debug_prints
@@ -39,6 +43,10 @@ module Osaka
39
43
  elsif (@@debug_info_format == :short_html)
40
44
  debug_output = applescript
41
45
  debug_output += "<br>"
46
+ elsif (@@debug_info_format == :script)
47
+ File.open(@@debug_info_script_filename, File::WRONLY|File::APPEND|File::CREAT, 0755) { |file|
48
+ file.puts("osascript#{escaped_commands}")
49
+ }
42
50
  end
43
51
  puts debug_output
44
52
  end
@@ -1,19 +1,19 @@
1
1
 
2
2
  module Osaka
3
3
  class TextEdit < TypicalApplication
4
- attr_accessor :wrapper
4
+ attr_accessor :control
5
5
 
6
6
  def initialize
7
7
  @name = "TextEdit"
8
- @wrapper = ApplicationWrapper.new("TextEdit")
8
+ @control = RemoteControl.new("TextEdit")
9
9
  end
10
10
 
11
11
  def type(text)
12
- @wrapper.keystroke(text)
12
+ control.keystroke(text)
13
13
  end
14
14
 
15
15
  def text
16
- @wrapper.get!("value", 'text area 1 of scroll area 1')
16
+ control.get!("value", 'text area 1 of scroll area 1')
17
17
  end
18
18
 
19
19
  end