dogtail.rb 5.71 KB
Newer Older
1
2
3
4
5
6
7
module Dogtail
  module Mouse
    LEFT_CLICK = 1
    MIDDLE_CLICK = 2
    RIGHT_CLICK = 3
  end

anonym's avatar
anonym committed
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  TREE_API_NODE_SEARCHES = [
    :button,
    :child,
    :childLabelled,
    :childNamed,
    :menu,
    :menuItem,
    :tab,
    :text,
    :textentry,
  ]

  TREE_API_NODE_ACTIONS = [
    :click,
    :doubleClick,
    :grabFocus,
    :keyCombo,
    :point,
    :typeText,
  ]

  TREE_API_APP_SEARCHES = TREE_API_NODE_SEARCHES + [
    :dialog,
    :window,
  ]

34
  # We want to keep this class immutable so that handles always are
anonym's avatar
anonym committed
35
  # left intact when doing new (proxied) method calls.  This way we
36
37
  # can support stuff like:
  #
anonym's avatar
anonym committed
38
39
40
41
42
  #     app = Dogtail::Application.new('gedit')
  #     menu = app.menu('Menu')
  #     menu.click()
  #     menu.something_else()
  #     menu.click()
43
44
45
46
47
48
49
  #
  # i.e. the object referenced by `menu` is never modified by method
  # calls and can be used as expected. This explains why
  # `proxy_call()` below returns a new instance instead of adding
  # appending the new component the proxied method call would result
  # in.

anonym's avatar
anonym committed
50
  class Application
51

anonym's avatar
anonym committed
52
53
    def initialize(app_name, opts = {})
      @app_name = app_name
54
      @opts = opts
anonym's avatar
anonym committed
55
56
57
58
59
60
61
      @init_lines = @opts[:init_lines] || [
        "from dogtail import tree",
        "from dogtail.config import config",
        "config.searchShowingOnly = True",
        "application = tree.root.application('#{@app_name}')",
      ]
      @components = @opts[:components] || ['application']
62
63
64
65
66
67
68
69
70
71
    end

    def build_script(lines)
      (
        ["#!/usr/bin/python"] +
        @init_lines +
        lines
      ).join("\n")
    end

anonym's avatar
anonym committed
72
73
74
75
    def build_line
      @components.join('.')
    end

76
    def run(lines = nil)
77
      @opts[:user] ||= LIVE_USER
78
79
80
      lines ||= [build_line]
      lines = [lines] if lines.class != Array
      script = build_script(lines)
81
82
83
84
85
86
87
88
89
90
91
92
      script_path = $vm.execute_successfully('mktemp', @opts).stdout.chomp
      $vm.file_overwrite(script_path, script, @opts[:user])
      args = ["/usr/bin/python '#{script_path}'", @opts]
      if @opts[:allow_failure]
        $vm.execute(*args)
      else
        $vm.execute_successfully(*args)
      end
    ensure
      $vm.execute("rm -f '#{script_path}'")
    end

93
94
95
96
97
98
99
100
101
102
103
104
105
106
    def self.value_to_s(v)
      if v == true
        'True'
      elsif v == false
        'False'
      elsif v.class == String
        "'#{v}'"
      elsif [Fixnum, Float].include?(v.class)
        v.to_s
      else
        raise "#{self.class.name} does not know how to handle argument type '#{v.class}'"
      end
    end

anonym's avatar
anonym committed
107
108
109
110
111
    # Generates a Python-style parameter list from `args`. If the last
    # element of `args` is a Hash, it's used as Python's kwargs dict.
    # In the end, the resulting string should be possible to copy-paste
    # into the parentheses of a Python function call.
    # Example: [42, {:foo => 'bar'}] => "42, foo = 'bar'"
112
    def self.args_to_s(args)
113
114
115
116
117
      args_list = args
      args_hash = nil
      if args_list.class == Array && args_list.last.class == Hash
        *args_list, args_hash = args_list
      end
118
      (
119
120
        (args_list.nil? ? [] : args_list.map { |e| self.value_to_s(e) }) +
        (args_hash.nil? ? [] : args_hash.map { |k, v| "#{k}=#{self.value_to_s(v)}" })
121
      ).join(', ')
122
123
    end

anonym's avatar
anonym committed
124
125
126
127
128
129
130
131
    def wait(timeout = nil)
      if timeout
        try_for(timeout) { run }
      else
        run
      end
    end

132
133
134
135
136
137
138
139
    # Equivalent to the Tree API's Node.findChildren(), with the
    # arguments constructing a GenericPredicate to use as parameter.
    def children(*args)
      # A fundamental assumption of ScriptProxy is that we will only
      # act on *one* object at a time. If we were to allow more, we'd
      # have to port looping, conditionals and much more into our
      # script generation, which is insane.
      # However, since references are lost between script runs (=
anonym's avatar
anonym committed
140
      # Application.run()) we need to be a bit tricky here. We use the
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
      # internal a11y AT-SPI "path" to uniquely identify a Dogtail
      # node, so we can give handles to each of them that can be used
      # later to re-find them.
      find_paths_script_lines = [
        "from dogtail import predicate",
        "for n in #{build_line}.findChildren(predicate.GenericPredicate(#{self.class.args_to_s(args)})):",
        "    print(n.path)",
      ]
      a11y_at_spi_paths = run(find_paths_script_lines).stdout.chomp.split("\n")
      a11y_at_spi_paths.map do |path|
        more_init_lines = [
          "from dogtail import predicate",
          "node = None",
          "for n in #{build_line}.findChildren(predicate.GenericPredicate()):",
          "    if str(n.path) == '#{path}':",
          "        node = n",
          "        break",
anonym's avatar
anonym committed
158
          "assert(node)",
159
        ]
anonym's avatar
anonym committed
160
161
162
163
164
165
166
        Node.new(
          @app_name,
          @opts.merge(
            init_lines: @init_lines + more_init_lines,
            components: ['node']
          )
        )
167
168
169
      end
    end

anonym's avatar
anonym committed
170
171
    def get_field(key)
      run("print(#{build_line}.#{key})").stdout
172
173
    end

anonym's avatar
anonym committed
174
175
176
177
    def set_field(key, value)
      run("#{build_line}.#{key} = #{self.class.value_to_s(value)}")
    end

anonym's avatar
anonym committed
178
179
180
181
182
183
184
185
186
187
    def proxy_call(method, args)
      args_str = self.class.args_to_s(args)
      method_call = "#{method.to_s}(#{args_str})"
      Node.new(
        @app_name,
        @opts.merge(
          init_lines: @init_lines,
          components: @components + [method_call]
        )
      )
188
189
    end

anonym's avatar
anonym committed
190
191
192
193
    TREE_API_APP_SEARCHES.each do |method|
      define_method(method) do |*args|
        proxy_call(method, args)
      end
194
195
    end

anonym's avatar
anonym committed
196
197
198
  end

  class Node < Application
199

anonym's avatar
anonym committed
200
201
202
203
    TREE_API_NODE_SEARCHES.each do |method|
      define_method(method) do |*args|
        proxy_call(method, args)
      end
204
205
    end

anonym's avatar
anonym committed
206
    TREE_API_NODE_ACTIONS.each do |method|
207
      define_method(method) do |*args|
anonym's avatar
anonym committed
208
        proxy_call(method, args).run
209
210
211
212
213
      end
    end

  end
end