# -*- coding: UTF-8 -*-
#
# Tencent is pleased to support the open source community by making QTA available.
# Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
# Licensed under the BSD 3-Clause License (the "License"); you may not use this 
# file except in compliance with the License. You may obtain a copy of the License at
# 
# https://opensource.org/licenses/BSD-3-Clause
# 
# Unless required by applicable law or agreed to in writing, software distributed 
# under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
#

'''定义Android控件
'''

import io
import os
import tempfile
import time

import six
from testbase.util import LazyInit, Timeout
from tuia.exceptions import ControlNotFoundError

from qt4a.androiddriver.androiddriver import AndroidDriver
from qt4a.androiddriver.util import Deprecated, get_intersection, logger

def lazy_init(func):
    '''调用到此函数时进入延迟初始化逻辑
    '''
    def wrap_func(self, *args, **kwds):
        self._lazy_obj._app  # 调到窗口类的延迟初始化函数
        return func(self, *args, **kwds)
    return wrap_func

class Window(object):
    '''控件容器基类
    '''
    Process = ''  # 所在的进程名，不填表示是主进程
    Activity = ''  # 所在的Activity

    def __init__(self, app_or_driver, wait_activity=True, **kwds):
        self._locators = {}
        self._device = None
        self._app = None
        if hasattr(app_or_driver, '_drivers'):
            # AndroidApp类型
            self._driver = app_or_driver.get_driver(self.Process)
            self._device = app_or_driver.device
            self._app = app_or_driver
        elif hasattr(app_or_driver, '_driver'):
            # Window类型
            self._app = app_or_driver._app
            if self._app:
                self._driver = app_or_driver._app.get_driver(self.Process)
            else:
                self._driver = app_or_driver._driver
            self._device = app_or_driver.device

        else:
            # AndroidDriver类型，一般用于调试
            self._driver = app_or_driver
        
        if not isinstance(self._driver, AndroidDriver):
            raise RuntimeError('参数类型错误：%s' % self._driver.__class__)
        self._wait_activity = wait_activity
        if wait_activity:
            self._wait_timeout = kwds.get('wait_timeout')
            self._wait_interval = kwds.get('wait_interval')
        self._lazy_obj = LazyInit(self, '_lazy_obj', self._init_window)

    def _init_window(self):
        '''延迟初始化时执行
        '''
        self._lazy_obj = self  # 避免多次调用初始化函数
        if self._wait_activity == True:
            wait_timeout = self._wait_timeout
            if not wait_timeout: wait_timeout = 12
            wait_interval = self._wait_interval
            if not wait_interval: wait_interval = 0.5
            self.wait_for_exist(wait_timeout, wait_interval)
        self.post_init()
        return self
    
    def post_init(self):
        '''窗口类自定义的初始化逻辑
        '''
        pass
    
    @property
    def Controls(self):
        '''返回控件集合。使用如foo.Controls['最小化按钮']的形式获取控件
        '''
        return self
    
    def hasControlKey(self, control_key):
        '''是否包含控件control_key
        
        :rtype: boolean
        '''
        return (control_key in self._locators)
    
    @property
    def device(self):
        return self._device

    def __findctrl_recur(self, ctrlkey):
        if not (ctrlkey in self._locators.keys()):
            raise NameError("%s没有名为'%s'的子控件！" % (type(self), ctrlkey))
        params = self._locators[ctrlkey].copy()
        ctrltype = params['type']
        del params['type']
        for key in params:
            value = params[key]
            if isinstance(value, six.string_types) and value.startswith('@'):
                params[key] = self[value[1:]]  # 使用缓存
        if issubclass(ctrltype, View):
            return ctrltype(**params)
        else:
            root = params.pop('root', None)
            if root == None: root = self
            params.pop('driver', None)  # delete unexpected param
            params.pop('activity', None)
            return ctrltype(root, ctrlkey, **params)
            
    @lazy_init
    def __getitem__(self, index):
        '''获取index指定控件
        
        :type index: string
        :param index: 控件索引，如'查找按钮'  
        '''
        if not (index in self._locators.keys()):
            raise NameError("%s没有名为'%s'的子控件！" % (type(self), index))
        if not '_instance' in self._locators[index]:
            instance = self.__findctrl_recur(index)
            self._locators[index]['_instance'] = instance
        return self._locators[index]['_instance']
    
    @Deprecated('update_locator')
    def updateLocator(self, locators):
        self.update_locator(locators)
    
    def update_locator(self, locators):
        '''更新控件定义
        '''
        for key in locators.keys():
            locators[key]['driver'] = self._driver  # 保存driver实例
            locators[key]['activity'] = self.__class__.Activity
        self._locators.update(locators)
        
    def wait_for_exist(self, timeout=10, interval=0.5):
        '''等待窗口出现
        '''
        import re
        time0 = time.time()
        current_activity = ''
        if self.Activity == '': return True
        pattern = re.compile(self.Activity)
        while time.time() - time0 < timeout:
            current_activity = self.device.current_activity
            if current_activity == self.Activity:
                return True
            if current_activity and pattern.match(current_activity):
                return True
            time.sleep(interval)
        raise ControlNotFoundError('窗口：%s 未找到，当前窗口为：%s' % (self.__class__.Activity, current_activity))
    
    @property
    @lazy_init
    def rect(self):
        '''窗口区域
        '''
        root = self._driver.get_control(self.Activity, None, [])
        if root == 0:
            raise RuntimeError('查找根节点失败')
        rect = self._driver.get_control_rect(root)
        return rect['Left'], rect['Top'], rect['Width'], rect['Height']

    @lazy_init
    def close(self):
        '''关闭窗口
        '''
        if self.Activity == '':
            self._driver.send_key('{BACK}')
        else:
            if not self._driver.close_activity(self.Activity):
                self._driver.send_key('{BACK}')

        timeout = 3
        time0 = time.time()
        while time.time() - time0 < timeout:
            if self.device.current_activity != self.Activity: return
            time.sleep(0.5)
    
    def get_metis_view(self):
        '''返回MetisView
        '''
        from qt4a.mtcontrols import MetisView
        return MetisView(self)
    
class Gravity(object):
    '''用于控制控件靠左、居中等位置的属性
    '''
    NO_GRAVITY = 0x0000
    AXIS_SPECIFIED = 0x0001
    AXIS_PULL_BEFORE = 0x0002
    AXIS_PULL_AFTER = 0x0004
    AXIS_CLIP = 0x0008
    AXIS_X_SHIFT = 0
    AXIS_Y_SHIFT = 4

    TOP = (AXIS_PULL_BEFORE | AXIS_SPECIFIED) << AXIS_Y_SHIFT
    BOTTOM = (AXIS_PULL_AFTER | AXIS_SPECIFIED) << AXIS_Y_SHIFT
    LEFT = (AXIS_PULL_BEFORE | AXIS_SPECIFIED) << AXIS_X_SHIFT
    RIGHT = (AXIS_PULL_AFTER | AXIS_SPECIFIED) << AXIS_X_SHIFT
    CENTER_VERTICAL = AXIS_SPECIFIED << AXIS_Y_SHIFT
    FILL_VERTICAL = TOP | BOTTOM
    CENTER_HORIZONTAL = AXIS_SPECIFIED << AXIS_X_SHIFT
    FILL_HORIZONTAL = LEFT | RIGHT
    CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL
    FILL = FILL_VERTICAL | FILL_HORIZONTAL
    CLIP_VERTICAL = AXIS_CLIP << AXIS_Y_SHIFT
    CLIP_HORIZONTAL = AXIS_CLIP << AXIS_X_SHIFT
    RELATIVE_LAYOUT_DIRECTION = 0x00800000
    HORIZONTAL_GRAVITY_MASK = (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_X_SHIFT
    VERTICAL_GRAVITY_MASK = (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_Y_SHIFT
    DISPLAY_CLIP_VERTICAL = 0x10000000
    DISPLAY_CLIP_HORIZONTAL = 0x01000000
    START = RELATIVE_LAYOUT_DIRECTION | LEFT
    END = RELATIVE_LAYOUT_DIRECTION | RIGHT
    RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END

    def __init__(self, grav):
        self._gravity = grav

    @property
    def left(self):
        raise NotImplementedError()

    @property
    def right(self):
        raise NotImplementedError()

    @property
    def center(self):
        raise NotImplementedError()


def func_wrap(func):
    '''用于方法包装，主要作用是发现控件失效时能够更新控件
    '''
    def _func(*args, **kwargs):
        from qt4a.androiddriver.util import logger, ControlExpiredError
        try:
            return func(*args, **kwargs)
        except ControlExpiredError as e:
            view = args[0]
            try:
                view._update()
            except RuntimeError:
                logger.exception('update control failed')
                raise e  # 更新失败时直接抛出控件失效异常
            return func(*args, **kwargs)
    return _func

class VerticalSwipe(object):
    '''纵向滑动包装类
    '''
    def __init__(self, view):
        self._view = view
        self._is_class = isinstance(view, type)
        
    def __call__(self, *args, **kwds):
        if not self._is_class:
            raise RuntimeError('对象不支持__call__')
        view = self._view(*args, **kwds)
        return VerticalSwipe(view)
    
    def __str__(self):
        if not self._is_class:
            return '<VerticalSwipe_%s object at 0x%.8X>' % (self._view.__class__.__name__, id(self))
        else:
            return '<class \'VerticalSwipe_%s\'>' % self._view.__name__
        
    @property
    def rect(self):
        '''left, top, width, height
        '''
        self_rect = self._view.rect
        root = self._view._root

        root_list = []
        while not isinstance(root, Window):
            if isinstance(root, ScrollView):
                root_list.append(root)
            root = root._root
            
        if len(root_list) == 0: return self_rect
        root_rect = root_list[-1].rect  # 获取可滚动的最顶层根
        left = self_rect[0] if self_rect[0] > root_rect[0] else root_rect[0]
        top = self_rect[1] if self_rect[1] > root_rect[1] else root_rect[1]
        width = self_rect[0] + self_rect[2] - left if self_rect[0] + self_rect[2] < root_rect[0] + root_rect[2] else root_rect[0] + root_rect[2] - left
        height = self_rect[1] + self_rect[3] - top if self_rect[1] + self_rect[3] < root_rect[1] + root_rect[3] else root_rect[1] + root_rect[3] - top
        return left, top, width, height  # 取交集
        
    def swipe_up(self):
        '''向上滑动
        '''
        rect = self.rect
        x1 = x2 = rect[0] + rect[2] // 2
        y1 = rect[1] + rect[3] * 3 // 4
        y2 = rect[1] + rect[3] // 4
        self._view._driver.drag(x1, y1, x2, y2)
    
    def swipe_down(self):
        '''向下滑动
        '''
        rect = self.rect
        x1 = x2 = rect[0] + rect[2] // 2
        y1 = rect[1] + rect[3] // 4
        y2 = rect[1] + rect[3] * 3 // 4
        self._view._driver.drag(x1, y1, x2, y2)
    
    def __getattr__(self, attr):
        return getattr(self._view, attr)
    
class View(object):
    '''控件基类
    '''
    def __init__(self, activity, root, driver, locator=None, hashcode=0):
        self._root = root
        if locator != None:
            self._qpath = locator
            self._locator = locator._parsed_qpath  # 解析后的list
        else:
            if hashcode == 0:
                raise RuntimeError('参数错误')
            self._locator = []
        self._driver = driver
        self._activity = activity
        self._hashcode = 0  # 每个Java对象实例的hashcode（根据内存地址计算出来）
        if hashcode != 0:
            self._hashcode = hashcode
        self._need_convert_qpath = True  # 是否需要转换QPath的ID为整型ID
        
    def __eq__(self, other):
        '''根据hashcode判断两个控件是否相同
        '''
        if other == None: return False
        if self.hashcode == 0 or other.hashcode == 0:
            raise RuntimeError('hashcode错误')
        return self.hashcode == other.hashcode

    def __str__(self):
        return '<%s(id=0x%X) at 0x%X>' % (self.__class__.__name__, self.hashcode & 0xFFFFFFFF, id(self))
    
    def _handle_qpath(self, qpath):
        '''对qpath进行处理，返回处理过的QPath列表
        '''
        # 将字符串ID改为整型ID
        import copy, itertools
        qpath = copy.deepcopy(qpath)  # 防止qpath被修改
        
        result = []  # 返回的是可能的qpath的列表
        for item in qpath:
            item_list = []
            if 'Id' in item:
                # TODO: 如果有两级ID，其中一级是数字形式的
                id_list = self.container._app._get_view_id(item['Id'][1])  # 可能会有多个整型ID
                if id_list == None: 
                    item_list.append(item)
                    result.append(item_list)
                    continue
                if len(id_list) == 1:
                    item['Id'][1] = str(id_list[0])
                    item_list.append(item)
                else:
                    # 存在多个id
                    for _id in id_list:
                        item['Id'][1] = str(_id)
                        item_list.append(copy.deepcopy(item))
            else:
                item_list.append(item)
            result.append(item_list)
            
        qpath_list = []
        for qpath in itertools.product(*result):
            qpath_list.append(qpath)
        return qpath_list
    
    @property
    def container(self):
        '''获取所在容器类
        '''
        root = self._root
        if isinstance(root, Window):
            return root
        else:
            return root.container

    def _get_hashcode(self, parent=0, locator=None):
        '''找到控件的hashcode值
        '''
        if locator: self._locator = locator
        if not self._locator: raise RuntimeError('控件定位信息缺失')
        if parent == 0 and isinstance(self._root, View):
            # 存在父节点
            if self._root._hashcode != 0:
                parent = self._root._hashcode
            else:
                parent = self._root._get_hashcode()
                if parent == 0: return 0  # 父节点不存在
                self._root._hashcode = parent
        
        idx = 0
        if isinstance(self._locator, list):
            # 先使用原始QPath查询一次
            result = self._driver.get_control(self._activity, parent, self._locator)
            if result != 0: 
                if self._need_convert_qpath: self._need_convert_qpath = False  # 不再需要转换QPath
                return result  # TODO: 处理"控件索引超出范围"情况
            if not self._need_convert_qpath: return 0  # 避免控件消失后还要再去获取整型ID
            if not self.container._app._is_use_int_view_id(): return 0  # 不使用整型ID
            
            import re
            pattern = re.compile('^0x[0-9a-fA-F]+$')
            
            need_convert = False
            for item in self._locator:
                if 'Id' in item:
                    if item['Id'][1].isdigit(): continue
                    if pattern.match(item['Id'][1]): continue  # 16进制的
                    need_convert = True
                    break
            if not need_convert: return 0  # 已经是数字类型的ID了
            
            # 生成整型ID的QPath
            qpath_list = self._handle_qpath(self._locator)  
            if len(qpath_list) == 1:
                self._locator = (self._locator, qpath_list[0])
            elif len(qpath_list) > 1:
                # 有多个QPath
                qpath_list.insert(0, self._locator)
                self._locator = tuple(qpath_list)  # tuple类型
            else:
                raise RuntimeError('QPath解析错误：%s' % self._locator)
            idx = 1  # 第一个不需要检查了
            
        for loc in self._locator[idx:]:
            result = self._driver.get_control(self._activity, parent, loc)
            if result != 0: 
                self._locator = list(loc)  # 如果找到,认为这是正确的QPath,以后只使用该QPath进行查找
                self._need_convert_qpath = False
                return result  # 找到即返回
        return 0
            
    @property
    def hashcode(self):
        '''控件唯一标识，只有真正访问控件信息时才会去获取该标识
        '''
        if self._hashcode == 0:
            # 尚未获取控件的hashcode
            timeout = 10  # 查找控件的超时时间
            interval = 0.1
            time0 = time.time()
            while time.time() - time0 < timeout:
                self._hashcode = self._get_hashcode()
                if self._hashcode == 0:
                    # 等待一段时间再查找
                    time.sleep(interval)
                else:
                    break
            if self._hashcode == 0:
                root = self._root
                root_list = []
                while isinstance(root, View):
                    root_list.append(root)
                    root = root._root
                qpath = self._qpath
                locator = self._locator
                parent = self._root
                if len(root_list) > 0:
                    for i in range(len(root_list) - 1, -1, -1):
                        if root_list[i]._hashcode == 0:
                            qpath = root_list[i]._qpath
                            locator = root_list[i]._locator
                            parent = root_list[i]._root
                            break
                parent = parent._hashcode if isinstance(parent, View) else 0
                pos = self._driver.get_control(self._activity, parent, locator, True)
                qpath = str(qpath)
                split_char = qpath[0]
                qpath_list = qpath[1:].split(split_char)
                err_qpath = split_char.join(qpath_list[pos:])
                if err_qpath: err_qpath = split_char + err_qpath  # 补上前面的分隔符
                err_msg = '进程【%s】中未找到控件：%s \n未找到部分路径为：【%s】' % (self._driver._process_name, qpath, err_qpath)
                current_activity = self.container.device.get_current_activity()
                view_tree = self._driver.get_control_tree(current_activity)
                logger.info('Current acvitiy %s control tree：\n%s' % (current_activity.encode('utf8'), view_tree.encode('utf8')))
                raise ControlNotFoundError(err_msg)
        return self._hashcode

    def _update(self):
        '''更新hashcode
        '''
        if isinstance(self._root, View):
            self._root._update()
        self._hashcode = 0
        return self.hashcode

    @property
    @func_wrap
    def parent(self):
        '''获取父控件
        '''
        if not hasattr(self, '_parent'):
            self._parent = self._driver.get_parent(self.hashcode)
        return View(hashcode=self._parent, activity=self._activity, root=self.container, driver=self._driver)

    @property
    @func_wrap
    def children(self):
        '''获取子节点列表
        '''
        children = self._driver.get_children(self.hashcode)
        return [View(hashcode=child, root=self, activity=self._activity, driver=self._driver) for child in children]

    @property
    @func_wrap
    def rect(self):
        '''left, top, width, height
        '''
        get_rect_interval = 0.1  # 防止频繁获取控件坐标信息
        if not hasattr(self, '_last_get_rect_time') or (time.time() - self._last_get_rect_time) > get_rect_interval:
            rect = self._driver.get_control_rect(self.hashcode)
            result = rect['Left'], rect['Top'], rect['Width'], rect['Height']
            self._last_get_rect_time = time.time()
            self._last_rect = result
        return self._last_rect

    @property
    @func_wrap
    def visible(self):
        '''是否可见
        '''
        return self._driver.get_control_visibility(self.hashcode)

    @property
    @func_wrap
    def _clickable(self):
        '''是否可点击
        '''
        CLICKABLE = 0x00004000
        flags = self._driver.get_object_field_value(self.hashcode, 'mViewFlags')
        return int(flags) & CLICKABLE == CLICKABLE

    @property
    def clickable(self):
        '''是否可点击
        to be deleted
        '''
        return self._clickable

    @property
    @func_wrap
    def enabled(self):
        '''是否可用
        '''
        ENABLE = 0x00000000
        ENABLED_MASK = 0x00000020
        flags = int(self._driver.get_object_field_value(self.hashcode, 'mViewFlags'))
        return flags & ENABLED_MASK == ENABLE

    @property
    @func_wrap
    def background_color(self):
        '''背景色
        '''
        result = self._driver.get_object_field_value(self.hashcode, 'mBackground.mColorState.mUseColor')
        if result == 'null':
            return None
        return int(result)

    @property
    def content_desc(self):
        '''控件描述
        '''
        return self._driver.get_object_field_value(self.hashcode, 'mContentDescription')
    
    def exist(self):
        '''判断控件是否存在
        '''
        from qt4a.androiddriver.util import ControlExpiredError, logger
        try:
            self._hashcode = self._get_hashcode()
            return self._hashcode != 0  # 由于Java端使用int型存储，因此可能为负数
        except ControlExpiredError:
            return False
        except RuntimeError:
            logger.exception('获取控件失败')  # 如果没有获取到控件ID对应的整型ID，认为控件不存在
            return False
        
    def wait_for_exist(self, timeout=10, interval=0.1):
        '''等待控件出现
        '''
        time0 = time.time()
        while time.time() - time0 < timeout:
            if self.exist(): return True
            time.sleep(interval)
        raise ControlNotFoundError('控件：%s 未找到' % self._qpath)

    def wait_for_visible(self, timeout=10, interval=0.2):
        '''等待控件可见
        
        :param timeout:  超时时间，单位：秒
        :type  timeout:  int/float
        :param interval: 重试间隔时间，单位：秒
        :type  interval: int/float
        '''
        time0 = time.time()
        while time.time() - time0 < timeout:
            if self.visible: return
            time.sleep(interval)
        raise RuntimeError('Control is not visible in %s seconds' % timeout)
    
    def wait_for_invisible(self, timeout=10, interval=0.2):
        '''等待控件不可见
        
        :param timeout:  超时时间，单位：秒
        :type  timeout:  int/float
        :param interval: 重试间隔时间，单位：秒
        :type  interval: int/float
        '''
        time0 = time.time()
        while time.time() - time0 < timeout:
            if not self.visible: return
            time.sleep(interval)
        raise RuntimeError('Control is not invisible in %s seconds' % timeout)
    
    def _point_in_view(self, x, y):
        '''判断点(x, y)是否在当前View可视范围内
        '''
        view_rect = self.rect
        if x < view_rect[0] or x >= view_rect[0] + view_rect[2]: return False
        if y < view_rect[1] or y >= view_rect[1] + view_rect[3]: return False
        return True
    
    def _get_scroll_root(self):
        '''获取当前控件可滚动区域的根控件
        '''
        root = self._root
        if isinstance(root, ListItem):
            root = root._root  # 获取ListView视图

        root_list = []
        while not isinstance(root, Window):
            if isinstance(root, (ScrollView, ViewPager)):
                root_list.append(root)
            root = root._root
        if root_list == []: return None
        return root_list[-1]  # 获取可滚动的最顶层根
        
    def scroll_to_visible(self, root_rect=None, self_rect=None):
        '''滚动到可视区域
        '''
        root = self._get_scroll_root()
        if root == None: return 0, 0  # 不需要滚动
        
        if not self_rect: self_rect = self.rect
        if not root_rect: root_rect = root.rect
        visible_rect = get_intersection(self_rect, root_rect)
        min_clickable_area = (20, 10)  # 最小可点击区域的大小
        _, screen_height = self.container.device.screen_size
        if visible_rect[0] >= 0 and visible_rect[1] >= 0 and visible_rect[2] >= min_clickable_area[0] and visible_rect[3] >= min_clickable_area[1] and screen_height - (visible_rect[1] + visible_rect[3]) >= 50: 
            logger.info('intersect area is %s' % (visible_rect,))
            return 0, 0
        
        if self_rect[2] > root_rect[2] or self_rect[3] > root_rect[3]:
            x_visible = True
            y_visible = True
            if self_rect[2] >= root_rect[2]:
                # 控件宽度大于容器宽度
                max_left = self_rect[0] if self_rect[0] > root_rect[0] else root_rect[0]
                min_right = self_rect[0] + self_rect[2] if self_rect[0] + self_rect[2] < root_rect[0] + root_rect[2] else root_rect[0] + root_rect[2]
                visible_width = min_right - max_left
                if visible_width < root_rect[2] // 2:
                    # 由于此时控件不可能完全可见，因此认为一半可见即为可见
                    x_visible = False
            if self_rect[3] >= root_rect[3]:
                # 控件高度大于容器高度
                max_top = self_rect[1] if self_rect[1] > root_rect[1] else root_rect[1]
                min_bottom = self_rect[1] + self_rect[3] if self_rect[1] + self_rect[3] < root_rect[1] + root_rect[3] else root_rect[1] + root_rect[3]
                visible_height = min_bottom - max_top
                if visible_height < root_rect[3] // 2:
                    y_visible = False

            if x_visible and y_visible: return 0, 0
            
        # print (self_rect, root_rect)

        if isinstance(root, ViewPager):
            offset = self_rect[0] - root_rect[0]
            count = offset // root_rect[2]
            root.scroll(count, root_rect)
            return root_rect[2] * count, 0

        x_offset = y_offset = 0
        if self_rect[0] < root_rect[0]:
            # 需要向右滑动
            x_offset = self_rect[0] - root_rect[0]
        elif self_rect[0] + self_rect[2] > root_rect[0] + root_rect[2]:
            # 理论上不会出现左右边界都超出root边界范围的情况
            x_offset = (self_rect[0] + self_rect[2]) - (root_rect[0] + root_rect[2])
        if self_rect[1] < root_rect[1]:
            # 需要向下滑动
            y_offset = self_rect[1] - root_rect[1]
        elif self_rect[1] + self_rect[3] > root_rect[1] + root_rect[3]:
            # print (self_rect[1], self_rect[3], root_rect[1], root_rect[3])
            y_offset = (self_rect[1] + self_rect[3]) - (root_rect[1] + root_rect[3])

        if abs(x_offset) > 5 or abs(y_offset) > 5:
            # 低于5个像素不滚动
            # print (x_offset, y_offset)
            root.scroll(x_offset, y_offset)
            time.sleep(0.5)  # 测试发现，滑动后等待时间过短就点击会无效
        return x_offset, y_offset

    def _pre_click(self, x_offset=None, y_offset=None):
        '''点击前的处理
        
        :param x_offset: 距离控件区域左上角的横向偏移。
        :type x_offset:  int或float
        :param y_offset: 距离控件区域左上角的纵向偏移。
        :type y_offset:  int或float
        
        :return 要点击的坐标
        '''

        visible = False
        rect_valid = False
        old_rect = None
        enable = False
        
        screen_width, screen_height = self.container.device.screen_size
        self.hashcode  # 确保控件存在
        root = self._get_scroll_root()
        root_rect = root.rect if root != None else [0, 0, screen_width, screen_height]
        if root != None:
            timeout = 2
            time0 = time.time()
            while time.time() - time0 < timeout:
                if root_rect[2] == 0 or root_rect[3] == 0 or root_rect[0] < 0 or root_rect[1] < 0 or \
                root_rect[0] + root_rect[2] > screen_width or root_rect[1] + root_rect[3] > screen_height:
                    time.sleep(0.1)
                    root_rect = root.rect
                else:
                    break
            else:
                logger.warn('invalid root rect: %s' % (root_rect,))
                
        timeout = 10
        time0 = time.time()
        while time.time() - time0 < timeout:
            # 尝试操作，会出现由于控件尚未初始化完成导致获取的rect不正确的情况
            if not visible:
                if not self.visible:
                    time.sleep(0.1)
                    continue
                else:
                    visible = True

            if not enable:
                # 检查是否可用
                enable = self.enabled
                if not enable:
                    time.sleep(0.1)
                    continue
            
            if not rect_valid:
                rect = self.rect
                if rect[2] == 0 or rect[3] == 0:  # 进行点击操作的控件长宽不可能为0
                    logger.debug('control %s width or height is 0 [%s]' % (self._hashcode, rect))
                    time.sleep(0.2)  # 间隔时间延长为0.2S
                    continue
                
                if not old_rect or rect != old_rect:
                    # 等待控件坐标稳定
                    old_rect = rect
                    logger.debug('wait for control %s stable' % self._hashcode)
                    time.sleep(0.2)
                    continue
                
                is_scroll = False  # 控件是否滚动
                if root != None:
                    for _ in range(5):
                        x_offset1, y_offset1 = self.scroll_to_visible(root_rect, rect)  # 自动滚动到可视区域
                        if abs(x_offset1) <= 5 and abs(y_offset1) <= 5:
                            break
                        else:
                            is_scroll = True
                            rect = self.rect  # 重新获取滚动后的坐标
                
                visible_rect = get_intersection(rect, root_rect)
                if is_scroll:
                    old_rect = visible_rect
                    time.sleep(0.2)
                else:
                    # 不需要再判断控件是否稳定
                    old_rect = None
                    rect = visible_rect
                rect_valid = True
            
            if old_rect != None:
                rect = get_intersection(self.rect, root_rect)
                if old_rect != rect:
                    # 防止有些控件加载后出现位移
                    old_rect = rect
                    time.sleep(0.2)
                    continue
            
            x = (rect[0] + rect[2] // 2)
            y = (rect[1] + rect[3] // 2)
            if x_offset: x = rect[0] + x_offset
            if y_offset: y = rect[1] + y_offset
            return x, y
        else:
            current_activity = self.container.device.get_current_activity()
            if current_activity != self._activity:
                import re
                ret = re.compile(self._activity)
                if not ret.match(current_activity): raise RuntimeError("当前Activity为%s，不是控件所属的Activity" % current_activity)
            if not visible: raise RuntimeError('控件不可见')
            if not enable: raise RuntimeError('控件不可用')
            if not rect_valid: raise RuntimeError('控件区域错误')
            raise RuntimeError('未知错误')
    
    def _click(self, click_time, x=None, y=None, check_ret=True):
        '''具有重试逻辑的点击
        
        :param click_time: 按住的时长，单位为秒
        :type click_time:  int/float
        :param x: 点击的横坐标
        :type x:  int/float
        :param y: 点击的纵坐标
        :type y:  int/float
        :param check_ret: 是否检查点击成功
        :type check_ret:  bool
        '''
        if not x or not y:
            rect = self.rect
            if not x: x = rect[0] + rect[2] // 2
            if not y: y = rect[1] + rect[3] // 2
        for _ in range(10):
            ret = self._driver.click(self.hashcode, x, y, click_time)
            if not check_ret or ret: return True
            time.sleep(0.5)
        raise RuntimeError('click (%d, %d) 失败' % (x, y))
    
    @func_wrap
    def click(self, x_offset=0, y_offset=0):
        '''单击
        
        :param x_offset: 距离控件区域左上角的横向偏移。
        :type x_offset:  int或float
        :param y_offset: 距离控件区域左上角的纵向偏移。
        :type y_offset:  int或float
        '''
        x, y = self._pre_click(x_offset, y_offset)
        self._click(0, x, y)
    
    def double_click(self, x_offset=0, y_offset=0):
        '''双击
        
        :param x_offset: 距离控件区域左上角的横向偏移。
        :type x_offset:  int或float
        :param y_offset: 距离控件区域左上角的纵向偏移。
        :type y_offset:  int或float
        '''
        x, y = self._pre_click(x_offset, y_offset)
        self._click(0, x, y)
        self._click(0, x, y)
        
    def multiple_click(self, count=3, x_offset=0, y_offset=0):
        '''多次点击
        
        :param count: 点击多少次
        :type count:  int
        :param x_offset: 距离控件区域左上角的横向偏移。
        :type x_offset:  int或float
        :param y_offset: 距离控件区域左上角的纵向偏移。
        :type y_offset:  int或float
        '''
        if count < 1: return
        x, y = self._pre_click(x_offset, y_offset)
        for _ in range(count):
            self._click(0, x, y)
    
    @func_wrap
    def long_click(self, duration=1, x_offset=0, y_offset=0, sync=True):
        '''长按
        
        :param duration: 按住时长，单位为秒
        :type duration:  int/float
        :param x_offset: 距离控件区域左上角的横向偏移。
        :type x_offset:  int/float
        :param y_offset: 距离控件区域左上角的纵向偏移。
        :type y_offset:  int/float
        :param sync:     是否是同步调用，为True表示等到长按结束才返回，False表示立即返回
        :type sync:      bool
        '''
        x, y = self._pre_click(x_offset, y_offset)
        if sync:
            self._click(duration, x, y, False)  # 长按不检查回调
        else:
            import threading
            # 按下后立即返回
            self._driver.drag(x, y, x, y, 0, 0, True, False)
            def _delay_func():
                time.sleep(duration)
                self._driver.drag(x, y, x, y, 0, 0, False, True)
            t = threading.Thread(target=_delay_func, args=())
            t.start()
            
    def wait_for_value(self, prop_name, prop_value, timeout=10, interval=0.5, regularMatch=False):
        """等待控件属性值出现, 如果属性为字符串类型，则使用正则匹配
        
        :param prop_name: 属性名字
        :param prop_value: 等待出现的属性值
        :param timeout: 超时秒数, 默认为10
        :param interval: 等待间隔，默认为0.5
        :param regularMatch: 参数 property_name和waited_value是否采用正则表达式的比较。默认为不采用（False）正则，而是采用恒等比较。
        """
        Timeout(timeout, interval).waitObjectProperty(self, prop_name, prop_value, regularMatch)
    
    def swipe(self, direct):
        '''滑动
        
        :param direct: 方向
        :type direct:  string，只能是“up”、“down”、“left”、“right”中的一个值
        '''
        rect = self.rect
        if direct == 'up':
            x1 = x2 = rect[0] + rect[2] // 2
            y1 = rect[1] + rect[3] * 2 // 3
            y2 = rect[1] + rect[3] // 3
        elif direct == 'down':
            x1 = x2 = rect[0] + rect[2] // 2
            y1 = rect[1] + rect[3] // 3
            y2 = rect[1] + rect[3] * 2 // 3
        elif direct == 'left':
            y1 = y2 = rect[1] + rect[3] // 2
            x1 = rect[0] + rect[2] * 2 // 3
            x2 = rect[0] + rect[2] // 3
        elif direct == 'right':
            y1 = y2 = rect[1] + rect[3] // 2
            x1 = rect[0] + rect[2] // 3
            x2 = rect[0] + rect[2] * 2 // 3
        else:
            raise RuntimeError('direct参数只能是：up、down、left、right中的一个')  
        self._driver.drag(x1, y1, x2, y2)
    
    def get_metis_view(self):
        '''返回MetisView
        '''
        from qt4a.mtcontrols import MetisView
        return MetisView(self)
    
class TextView(View):
    '''包含Text的View 
    '''
    @property
    @func_wrap
    def text(self):
        '''获取文本
        '''
        return self._driver.get_control_text(self.hashcode)
    
    @property
    def html_style_text(self):
        '''HTML格式文本
        '''
        return self._driver.get_control_text(self.hashcode, True)
    
    @text.setter
    def text(self, value):
        '''设置文本
        '''
        if not isinstance(value, six.string_types):
            value = str(value)
        if six.PY2 and not isinstance(value, unicode):
            try:
                value = value.decode('utf8')
            except UnicodeDecodeError:
                raise RuntimeError('参数编码错误：%r' % value)
        self.wait_for_visible()
        self.disable_soft_input()  # 赋值前关闭软键盘
        for _ in range(3):
            if self._driver.set_control_text(self.hashcode, value): return
            self.click()
        raise RuntimeError('设置控件文本失败')
    
    @property
    def text_size(self):
        '''字体大小
        '''
        ret = self._driver.call_object_method(self.hashcode, 'mTextPaint', 'getTextSize')
        return int(ret)
    
    @property
    def text_color(self):
        '''字体颜色
        '''
        import json
        ret = json.loads(self._driver.get_object_field_value(self.hashcode, 'mCurTextColor'))
        ret = int(ret)
        if ret < 0: ret += 0x100000000
        return ret
    
    @func_wrap
    def disable_soft_input(self):
        '''禁用软键盘
        '''
        self._driver.enable_soft_input(self.hashcode, False)
    
    def _get_char_rect(self, start_offset, end_offset):
        '''根据字符偏移量计算区域坐标
        
        :param start_offset: 起始字符偏移
        :type start_offset:  int
        :param end_offset:   结束字符偏移
        :type end_offset:    int
        '''
        rect = self._driver.get_text_rect(self.hashcode, start_offset, end_offset)
        return rect['Left'], rect['Top'], rect['Width'], rect['Height']
    
    @property
    def hint_text(self):
        '''空白提示文本
        '''
        return self._driver.get_object_field_value(self.hashcode, 'mHint')
    
    @property
    def image_resource_name(self):
        '''图像资源名称
        '''
        result = self._driver.get_control_image_resource(self.hashcode)
        for key in result:
            result[key] = self.container._app._get_drawable_resource_origin_name(result[key])
        return result
    
    def click_clickable_span(self):
        '''点击TextView中的ClickableSpan区域
        '''
        import re
        pattern = re.compile(r'^<p.*>(.|\n)*<span onclick>(.+)</span>(.*)</p>\s*$')
        html = self.html_style_text
        ret = pattern.match(html)
        if not ret: raise RuntimeError('HTML文本格式错误: %r' % html)
        text = self.text
        start_offset = text.find(ret.group(2))
        end_offset = start_offset + len(ret.group(2))
        rect = self._get_char_rect(start_offset + 1, end_offset)

        textview_rect = self.rect
        x1 = textview_rect[0]
        y1 = textview_rect[1]
        x2 = rect[0] + rect[2] // 2
        y2 = rect[1] + rect[3] // 2
        self.click(x2 - x1, y2 - y1)
        
class EditText(TextView):
    '''输入文本框
    '''
    def send_text(self, text):
        '''输入按键，此方法不能输入中文和大写字母
        '''
        self.click()  # 先获取焦点
        time.sleep(0.1)
        self._driver.send_keys(text)

    def click(self):
        '''click后自动关闭输入法
        '''
        super(TextView, self).click()
        self.disable_soft_input()
        time.sleep(1)

class Button(TextView):
    '''按钮类
    '''
    pass

class CompoundButton(Button):
    '''可选按钮，一般用于实现Switch
    '''
    @property
    @func_wrap
    def checked(self):
        '''是否已选
        '''
        return self._driver.get_control_checked(self.hashcode)

    @checked.setter
    def checked(self, check=True):
        '''设置是否选择
        '''
        if self.checked == check: return
        self.click()

class RadioButton(CompoundButton):
    '''单选按钮
    '''
    pass

class CheckBox(CompoundButton):
    '''选择按钮
    '''
    pass

class CheckedTextView(TextView):
    '''可选文本框
    '''
    @property
    def checked(self):
        '''是否已选
        '''
        return self._driver.get_object_field_value(self.hashcode, 'mChecked') == 'true'

class ImageView(View):
    '''显示图片控件
    '''
    @property
    @func_wrap
    def resource_name(self):
        '''当前使用的图片资源名称
        '''
        ret = self._driver.get_control_background(self.hashcode)
        if ret: return self.container._app._get_drawable_resource_origin_name(ret)
        res_name = self._driver.get_control_image_resource(self.hashcode)
        return self.container._app._get_drawable_resource_origin_name(res_name)
    
    def capture(self, save_path=''):
        '''保存图片到本地
        '''
        if not save_path:
            import tempfile, os
            tmp_path = tempfile.mkdtemp()
            save_path = os.path.join(tmp_path, 'tmp.png')
        
        for _ in range(3):
            pic_data = self._driver.capture_control(self.hashcode)
            if len(pic_data) == 0: 
                time.sleep(1)
                continue
            f = open(save_path, 'wb')
            f.write(pic_data)
            f.close()
            return save_path
        
        raise RuntimeError('获取控件截图失败')
    
    def save(self, save_path=''):
        '''保存图片到本地
        to de deleted
        '''
        return self.capture(save_path)
    
class ImageButton(ImageView):
    pass


class DropdownView(View):
    pass


# 布局类
class ViewGroup(View):
    pass

class FrameLayout(ViewGroup):
    pass

class LinearLayout(ViewGroup):
    pass

class RelativeLayout(ViewGroup):
    '''
    '''
    @property
    def gravity(self):
        '''位置属性
        '''
        return Gravity(self._driver.get_control_gravity(self.hashcode))

class ProgressBar(View):
    '''进度条
    '''
    @property
    def progress(self):
        '''进度
        '''
        return self._driver.get_control_progress(self.hashcode)

class SeekBar(ProgressBar):
    '''可修改进度的进度条
    '''
    @property
    def progress(self):
        '''进度
        '''
        return super(SeekBar, self).progress
    
    @progress.setter
    def progress(self, new_progress):
        '''设置新的进度
        
        :param new_progress: 新进度值,取值范围:0-100
        :type new_progress:  float
        '''
        timeout = 10
        time0 = time.time()
        while time.time() - time0 < timeout:
            rect = self.rect
            if rect[2] == 0 or rect[3] == 0:
                time.sleep(0.2)
                continue
            break
        offset_y = rect[3] // 2
        offset_x = rect[2] * new_progress / 100.0
        self.click(offset_x, offset_y)
        
class ScrollView(FrameLayout):
    '''滚动视图
    '''

    @property
    def reach_top(self):
        '''滑动区域达到顶部
        '''
        scroll_rect = self._driver.get_control_scroll_rect(self.hashcode)
        return scroll_rect[1] == 0

    @property
    def reach_bottom(self):
        '''滑动区域达到底部
        '''
        scroll_rect = self._driver.get_control_scroll_rect(self.hashcode)
        return scroll_rect[1] + scroll_rect[3] >= scroll_rect[5]
    
    def _scroll(self, x, y, count=5, interval=0.04):
        '''横向或纵向滚动
        
        :param x: x > 0 时向左滑动，x = x1 - x2，滚动条向右
        :type x:  int
        :param y: y > 0 时向上滑动，y = y1 - y2，滚动条向下
        :type y:  int
        :param    count: 分为几次滑动
        :type     count:  int
        '''
        if y != 0: y = y * 100 // abs(y) if abs(y) < 100 else y  # 为防止在某个控件内滚动变成点击,设置最小滑动距离为100
        rect = self.rect
        
        mid_x = rect[0] + rect[2] // 2  # 中点
        mid_y = rect[1] + rect[3] // 2
        
        interval *= 1000  # 秒转换成毫秒
        if x != 0:
            x1 = mid_x + x // 2
            x2 = mid_x - x // 2
        else:
            x1 = x2 = mid_x
        
        if y != 0:
            y1 = mid_y + y // 2
            y2 = mid_y - y // 2
        else:
            y1 = y2 = mid_y
            
        self._driver.drag(x1, y1, x2, y2, count, interval)
        
    def scroll(self, x, y, count=5, interval=0.04):
        '''横向或纵向滚动
        
        :param x: x > 0 时向左滑动，x = x1 - x2，滚动条向右
        :type x:  int
        :param y: y > 0时向上滑动，y = y1 - y2，滚动条向下
        :type y:  int
        :param    count: 分为几次滑动
        :type     count:  int
        '''
        # 为避免在不可滑动区域滑动，每次滑动只在可滑动区域3/4处滑动
        time0 = time.time()
        while time.time() - time0 < 10:
            rect = self.rect
            if rect[2] == 0 or rect[3] == 0:
                time.sleep(0.5)
            else:
                break
            
        max_x = rect[2] * 3 // 4
        max_y = rect[3] * 3 // 4
        
        while abs(x) >= max_x:
            self._scroll(max_x * x // abs(x), 0)
            x -= max_x * x // abs(x)
        if x != 0:self._scroll(x, 0)
        
        while abs(y) >= max_y:
            self._scroll(0, max_y * y // abs(y))
            y -= max_y * y // abs(y)
        if y != 0:self._scroll(0, y)
        
    def on_scroll(self, x, y):
        time.sleep(0.5)

    def scroll_up_one_page(self):
        '''向上滑动一页
        '''
        if self.reach_top: return False
        rect = self.rect
        scroll_y = rect[3]
#        scroll_rect = self.driver.get_control_scroll_rect(self.hashcode)
#        if scroll_y > scroll_rect[1]:
#            scroll_y = scroll_rect[1]
        self.scroll(0, -scroll_y)
        self.on_scroll(0, -scroll_y)
        return True

    def scroll_down_one_page(self):
        '''向下滑动一页
        '''
        if self.reach_bottom: return False
        rect = self.rect
        scroll_y = rect[3]
#        scroll_rect = self.driver.get_control_scroll_rect(self.hashcode)
#        offset = scroll_rect[5] - scroll_rect[1] + scroll_rect[3]
#        if scroll_y > offset:
#            scroll_y = offset
        self.scroll(0, scroll_y)
        self.on_scroll(0, scroll_y)
        return True

    def scroll_to_top(self):
        '''滑动到顶部
        '''
        if self.reach_top: return
        rect = self.rect
        while not self.reach_top:
            self.scroll(0, -rect[3])
            self.on_scroll(0, -rect[3])
        self._wait_for_refresh_complete()  # 可能会出现刷新操作

    def scroll_to_bottom(self):
        '''滑动到底部
        '''
        if self.reach_bottom: return
        rect = self.rect
        while not self.reach_bottom:
            self.scroll(0, rect[3])
            self.on_scroll(0, rect[3])

    def _wait_for_refresh_complete(self, timeout=20):
        '''等待刷新完成
        '''
        outer_top = self.rect[1]
        if len(self.children) > 0:
            time0 = time.time()
            while time.time() - time0 < timeout:
                children = self.children
                if len(children) > 0:  # 没有子节点时等待
                    try:
                        inner_top = children[0].rect[1]
                    except RuntimeError as e:
                        logger.warn('_wait_for_refresh_complete error: %s' % e)
                        time.sleep(0.5)
                        continue
                    if inner_top <= outer_top: return True
                time.sleep(0.5)
        return False

    def pull_down_refresh(self):
        '''下拉刷新
        '''
        rect = self.rect
        while rect[2] == 0 or rect[3] == 0:
            time.sleep(0.1)
            rect = self.rect
        self.scroll_to_top()
        # self.scroll(0, -rect[3], 10, 0.1)  # 通过增加滑动次数增加时间，一般下拉时需要暂停一下才会触发刷新操作
        x = rect[0] + rect[2] // 2
        y1 = rect[1] + rect[3] // 4
        y2 = rect[1] + rect[3] * 3 // 4
        
        self._driver.drag(x, y1, x, y2, send_down_event=True, send_up_event=False)
        time.sleep(1)  # 按住不动
        self._driver.drag(x, y2, x, y2, send_down_event=False, send_up_event=True)
        
        self._wait_for_refresh_complete()
        time.sleep(1.5)  # 有些控件刷新完立即操作会不成功
        
class AbsListView(ScrollView):
    '''ListView和GridView基类
    '''
    def __init__(self, *args, **kwds):
        super(AbsListView, self).__init__(*args, **kwds)
        self._first_visible_position = 0  # 可见的第一个控件索引
        self._last_visible_position = 0  # 可见的最后一个索引
        self._item_count = 0  # 所有子节点个数
        self._children = []
        self._first_update = True  # 第一次更新时需要多做些操作

    def __iter__(self):
        '''迭代器
        '''
        self.update()
        for i in range(self.item_count):
            if i < self.first_position:
                # 需要往上滑动
                while i < self.first_position:
                    self.scroll_up_one_page()
                if i > self.last_position:  # 防止滚过
                    self.scroll(0, 100 * (i - self.last_position + 1))
                    self.update()
            elif i > self.last_position:
                # 需要往下滑动
                while i > self.last_position:
                    if i >= self.item_count: return
                    self.scroll_down_one_page()
                if i < self.first_position:
                    self.scroll(0, -100 * (self.first_position - i + 1))
                    self.update()

            idx = i - self.first_position
            if idx >= len(self._children):
                # 可能之前拉到的数据不正确
                self.update()
            if idx < 0 or idx >= len(self._children):
                raise IndexError('%d 不在范围[0, %d]中' % (idx, len(self._children) - 1))
            yield ListItem(self._children[idx])

    def __len__(self):
        self.update()
        return self.item_count

    def __getitem__(self, key):
        '''支持listview[i]方式访问子控件
        '''
        if isinstance(key, int):
            return self.get_child(key)
        else:
            raise TypeError('只支持整型索引')

    def __str__(self):
        return '<%s(Count=%d) at 0x%X>' % (self.__class__.__name__, len(self), id(self))

    @property
    def item_count(self):
        return self._item_count

    @property
    def first_position(self):
        return self._first_visible_position

    @property
    def last_position(self):
        return self._last_visible_position

    def on_scroll(self, x, y):
        super(AbsListView, self).on_scroll(x, y)
        self.update()
        if self.reach_top:
            self._wait_for_refresh_complete()

    @func_wrap
    def _update_list(self):
        listview_info = self._driver.get_listview_info(self.hashcode)
        self._item_count = listview_info['Count']
        self._first_visible_position = listview_info['FirstPosition']
        self._last_visible_position = listview_info['LastPosition']
        if self._item_count > 0:
            self._children = self.children  # 防止每次都访问

    def update(self):
        for _ in range(20):
            show_count = self._last_visible_position - self._first_visible_position + 1
            self._update_list()
            if self._first_visible_position >= 0 and self._last_visible_position >= 0 and len(self._children) == self._last_visible_position - self._first_visible_position + 1:
                # 防止控件树还在构建过程
                if not self._first_update:
                    show_count_now = self._last_visible_position - self._first_visible_position + 1
                    if show_count_now == show_count: return  # 刷新前后显示的节点数目不变才是稳定状态
                self.wait_for_complete()
                self._first_update = False
                return
            time.sleep(0.1)

    def get_child(self, idx):
        '''提供使用索引访问子元素的方法
        '''
        self.update()
        item_count = self.item_count
        if idx < 0:
            # 允许使用负数索引
            idx += item_count
        if idx < 0 or idx >= item_count:
            raise IndexError('index=%d超出list范围' % idx)
        if idx < self.first_position:
            # 需要往上滑动
            while idx < self.first_position:
                self.scroll_up_one_page()
        elif idx > self.last_position:
            # 需要往下滑动
            while idx > self.last_position:
                self.scroll_down_one_page()
        idx = idx - self.first_position
        if idx < 0 or idx >= len(self._children):
            raise IndexError('index=%d错误，不在范围[0, %d]中' % (idx, len(self._children) - 1))
        return ListItem(self._children[idx], idx)
    
    @property
    def reach_top(self):
        return self.reached_top
    
    @property
    def reached_top(self):
        '''滑动区域达到顶部
        '''
        for _ in range(3):
            self.update()
            if self.first_position > 0: return False
            if self.item_count == 0: return True
            if len(self._children) == 0: return True
            # 此时第一个子节点肯定可见
            rect = View.rect.fget(self)  # 子类可能会重载rect以修改滑动范围，此时不能根据子类的实现来计算
            try:
                crect = self._children[0].rect
            except RuntimeError as e:
                from qt4a.androiddriver.util import logger
                # logger.warn('reach_top error: %s' % e)
                logger.exception(e)
                continue
            return crect[1] >= rect[1]
    
    @property
    def reach_bottom(self):
        return self.reached_bottom
    
    @property
    def reached_bottom(self):
        '''滑动区域达到底部
        '''
        if self.item_count == 0: self.update()
        return self.last_position == self.item_count - 1

#    def scroll_to_top(self):
#        '''滑动到顶部
#        '''
#        if self.reach_top: return
#        rect = self.bounding_rect
#        scroll_total = 0
#        while not self.reach_top:
#            scroll_y = self.first_position * 100  # 以每一项100的高度计算
#            if scroll_y > rect[3]: scroll_y = rect[3]
#            print ('scroll', -scroll_y)
#            self.scroll(0, -scroll_y)
#            scroll_total += scroll_y
#        self.on_scroll(0, -scroll_total)
#    
    def scroll_up_one_page(self):
        '''向上滑动一页
        '''
        if self.reached_top: return False
        rect = self.rect
        scroll_y = rect[3]
        if scroll_y > self.first_position * 100:
            scroll_y = self.first_position * 100
        if scroll_y < 100: scroll_y = 100
        self.scroll(0, -scroll_y)
        self.on_scroll(0, -scroll_y)
        return True

    def scroll_down_one_page(self):
        '''向下滑动一页
        '''
        if self.reached_bottom: return False
        rect = self.rect
        scroll_y = rect[3]
        offset = (self.item_count - self.last_position) * 100
        if scroll_y > offset:
            scroll_y = offset
        self.scroll(0, scroll_y)
        self.on_scroll(0, scroll_y)
        return True

    def wait_for_complete(self, timeout=2):
        '''等待ListView控件变化，比如需要读取本地数据
        '''
        count0 = self.item_count
        first_position0 = self.first_position
        last_position0 = self.last_position

        time0 = time.time()
        while time.time() - time0 < timeout:
            time.sleep(0.2)
            self._update_list()
            if self._first_visible_position >= 0 and self._last_visible_position >= 0 and len(self._children) == self._last_visible_position - self._first_visible_position + 1:
                count = self.item_count
                first_position = self.first_position
                last_position = self.last_position
                if count == count0 and first_position == first_position0 and last_position == last_position0: return True
                count0 = count
                first_position0 = first_position
                last_position0 = last_position
        return False

class ListItem(View):
    '''为方便遍历AbsListView，表示AbsListView的直接子孩子
    '''
    def __init__(self, view, idx=None):
        '''
        :param view: AbsListView中每一个子节点
        :type view:  View
        '''
        self._view = view
        self._control_dict = {}  # 用于缓存
        self._idx = idx  # 项索引，用于重新查找控件
        
    def __getitem__(self, key):
        '''支持listitem[key]方式访问子节点
        :param key: 封装时定义的子节点名
        :type key:  string
        '''
        if key in self._control_dict:
            return self._control_dict[key]
        item = self._view.container[key]
        import copy
        result = copy.copy(item)  # 复制一个对象
        result._root = self  # 修改根节点为当前节点
        self._control_dict[key] = result
        return result

    def __getattr__(self, attr):
        '''委托self._view实现
        '''
        return getattr(self._view, attr)
    
    def _update(self):
        '''如果没有索引值直接抛异常
        '''
        from qt4a.androiddriver.util import logger
        logger.debug('ListItem update, idx=%s' % self._idx)
        if self._idx == None:
            raise RuntimeError('ListView无法重新获取控件')
        self._view._root.update()
        self._view = self._view._root._children[self._idx]  # 重新获取Item项
    
    def has(self, key):
        '''是否存在该节点
        
        :param key: 封装时定义的子节点名
        :type  key: string
        '''
        from qt4a.androiddriver.util import ControlExpiredError, logger
        item = self._view.container[key]
        # child = self._driver.get_control(self._activity, self.hashcode, item._locator)
        self._view._need_convert_qpath = True  # 避免判断多个节点存在时不会使用整型ID查找问题
        try:
            hashcode = self._view._get_hashcode(self._view.hashcode, item._locator)
            if hashcode == 0: return False
        except ControlExpiredError:
            logger.info('ControlExpiredError occur in has')
            if self._idx == None: return False
            self._update()
            return self.has(key)
        item._locator = self._locator  # 替换为真正的QPath
        return True

class ListView(AbsListView):
    '''列表视图
    '''
    pass

class GridView(AbsListView):
    '''格子视图
    '''

class TabWidget(View):
    '''Tab控件
    '''
    @property
    def selected_index(self):
        '''当前所选项的索引
        '''
        return self._driver.get_selected_tab_index(self.hashcode)

class WebkitWebView(View):
    '''4.4以下版本中的WebView
    '''
    
    def __init__(self, *args, **kwargs):
        super(WebkitWebView, self).__init__(*args, **kwargs)
    
    def eval_script(self, frame_xpaths, script):
        '''执行JavaScript
        '''
        from qt4a.androiddriver.util import AndroidSpyError, ControlExpiredError
        try:
            return self._driver.eval_script(self.hashcode, frame_xpaths, script)
        except ControlExpiredError as e:
            raise e
        except AndroidSpyError as e:
            try:
                from qt4w.util import JavaScriptError
            except ImportError:
                raise
            else:
                raise JavaScriptError(frame_xpaths, e.args[0])
            
class WebView(View):
    '''Web页面容器
    '''
    title = None
    url = None
    is_chromium = None
    
    def __init__(self, *args, **kwargs):
        self._args = args
        self._kwargs = kwargs
        super(WebView, self).__init__(*args, **kwargs)
        self._is_chromium = self.__class__.is_chromium
        if self._is_chromium == None: self._is_chromium = self.container.device.sdk_version >= 19  # Android 4.4以上使用Chromium内核
        self._webview_impl = None
    
    @property
    def webdriver_class(self):
        '''WebView对应的WebDriver类
        '''
        from qt4a.androiddriver.webdriver import AndroidWebDriver
        return AndroidWebDriver
    
    @property
    def webview_impl(self):
        '''
        '''
        if self._webview_impl == None:
            if not self._is_chromium:
                self._webview_impl = WebkitWebView(*self._args, **self._kwargs)
            else:
                try:
                    self._webview_impl = ChromiumWebView(title=self.__class__.title, url=self.__class__.url, *self._args, **self._kwargs)
                except ImportError:
                    # Fallback to custom WebView
                    self._webview_impl = WebkitWebView(*self._args, **self._kwargs)

        return self._webview_impl
    
    def eval_script(self, frame_xpaths, script):
        '''在指定frame中执行JavaScript，并返回执行结果（该实现需要处理js基础库未注入情况的处理）
        
        :param frame_xpaths: frame元素的XPATH路径，如果是顶层页面，怎传入“[]”
        :type frame_xpaths:  list
        :param script:       要执行的JavaScript语句
        :type script:        string
        '''
        return self.webview_impl.eval_script(frame_xpaths, script)
    
    @property
    def visible_rect(self):
        '''WebView控件可见区域的坐标信息
        '''
        return self.rect
            
    def disable_soft_input(self):
        '''禁用软键盘
        '''
        return self._driver.enable_soft_input(self.hashcode, False)
    
    def click(self, x_offset=None, y_offset=None):
        '''点击WebView中的某个坐标
        
        :param x_offset: 与WebView左上角的横向偏移量
        :type x_offset:  int/float
        :param y_offset: 与WebView左上角的纵向偏移量
        :type y_offset:  int/float
        '''
        if x_offset != None or y_offset != None:
            rect = self.rect
            x = (rect[0] + rect[2] // 2) if not x_offset else (rect[0] + x_offset)
            y = (rect[1] + rect[3] // 2) if not y_offset else (rect[1] + y_offset)
            return self._click(0, x, y)
        else:
            return super(WebView, self).click()
    
    def long_click(self, x_offset, y_offset, duration=1):
        '''长按WebView中的某个坐标
        
        :param x_offset: 与WebView左上角的横向偏移量
        :type x_offset:  int/float
        :param y_offset: 与WebView左上角的纵向偏移量
        :type y_offset:  int/float
        :param duration: 按住的持续时间
        :type duration:  int/float
        '''
        return super(WebView, self).long_click(duration, x_offset, y_offset)
    
    def send_keys(self, keys):
        '''向WebView中的输入框发送按键
        
        :param keys: 要发送的按键
        :type keys:  string
        '''
        self.container.device.send_text(keys)
        #self._driver.send_keys(keys)
    
    def drag(self, x1, y1, x2, y2):
        '''从(x1, y1)点滑动到(x2, y2)点
        
        :param x1: 起点横坐标
        :type x1:  int/float
        :param y1: 起点纵坐标
        :type y1:  int/float
        :param x2: 终点横坐标
        :type x2:  int/float
        :param y2: 终点纵坐标
        :type y2:  int/float
        '''
        self._driver.drag(x1, y1, x2, y2, 10, 0.1)
    
    def screenshot(self):
        '''当前WebView的截图
        :return: PIL.Image
        '''
        from PIL import Image
        temp_path = tempfile.mktemp('.jpg')
        self.container.device.take_screen_shot(temp_path)
        with open(temp_path, 'rb') as fp:
            image_data = fp.read()
        os.remove(temp_path)
        image = Image.open(io.BytesIO(image_data))
        left, top, width, height = self.rect
        image = image.crop((left, top, left + width, top + height))
        return image


class ChromiumWebView(WebkitWebView):
    '''Chromium内核的WebView
    '''
    debugger_instances = {}
    
    def __init__(self, url=None, title=None, *args, **kwargs):
        import chrome_master
        super(ChromiumWebView, self).__init__(*args, **kwargs)
        self._is_system_webview = False
        webview_types = self._driver.get_control_type(self.hashcode)
        for webview_type in webview_types:
            if webview_type == 'android.webkit.WebView':
                self._is_system_webview = True
                break
        self._url = url
        self._title = title
        
        self._device = self.container.device
        self._pid = self._device.adb.get_pid(self._driver._process_name)
        self._service_name = 'webview_devtools_remote_%d' % self._pid

        chrome_master.set_logger(logger)

        self._chrome_master = chrome_master.ChromeMaster(('localhost', self._pid), self.create_socket)  # 高版本Chromium内核必须要使用localhost的Host

        self._page_debugger = self.get_debugger()
        self._page_debugger.register_handler(chrome_master.RuntimeHandler)

    def create_socket(self):
        '''创建socket对象
        '''
        try:
            sock = self._device.adb.create_tunnel(self._service_name, 'localabstract')
            if sock: return sock
        except:
            logger.warn('create socket tunnel %s failed' % self._service_name)
        self._driver.set_webview_debugging_enabled(self.hashcode, True)
        return self.create_socket()
        
    def get_page_url(self):
        '''获取当前WebView页面的url
        '''
        if not self._is_system_webview: return self._url
        timeout = 10
        time0 = time.time()
        while time.time() - time0 < timeout:
            try:
                ret = super(ChromiumWebView, self).eval_script([], 'location.href')
            except:
                logger.exception('Read page url failed')
                return
            if ret != 'about:blank': return ret
            time.sleep(0.5)
        else:
            raise RuntimeError('Get page url failed')
    
    def get_debugger(self, timeout=10):
        '''获取当前页面的RemoteDebugger实例
        '''
        url = self.get_page_url()
        time0 = time.time()
        while time.time() - time0 < timeout:
            try:
                return self._chrome_master.find_page(url=url, title=self._title)  # 获取最后一个页面
            except RuntimeError as e:
                logger.warn('[%s] %s' % (self.__class__.__name__, e))
                time.sleep(0.5)
        else:
            raise RuntimeError('Find page timeout')

    def convert_frame_tree(self, frame_tree, parent=None):
        '''将frame tree转化为Frame对象
        '''
        from qt4w import util
        frame = util.Frame(frame_tree['frame']['id'], frame_tree['frame'].get('name'), frame_tree['frame']['url'])
        if parent: parent.add_child(frame)
        if 'childFrames' in frame_tree:
            for child in frame_tree['childFrames']:
                self.convert_frame_tree(child, frame)
        return frame
        
    def _get_frame_id_by_xpath(self, frame_xpaths, timeout=10):
        '''获取frame id
        '''
        from qt4a.androiddriver.webdriver import AndroidWebDriver
        from qt4w import util
        time0 = time.time()
        while time.time() - time0 < timeout:
            frame_tree = self._page_debugger.page.get_frame_tree()
            frame = self.convert_frame_tree(frame_tree)
            frame_selector = util.FrameSelector(AndroidWebDriver(self), frame)
            try:
                frame = frame_selector.get_frame_by_xpath(frame_xpaths)
            except util.ControlNotFoundError:
                pass
            else:
                return frame.id
            time.sleep(0.5)
        else:
            raise ControlNotFoundError('Find frame %s timeout' % ''.join(frame_xpaths))
        
    def eval_script(self, frame_xpaths, script):
        '''在指定frame中执行JavaScript，并返回执行结果
        
        :param frame_xpaths: frame元素的XPATH路径，如果是顶层页面，怎传入“[]”
        :type frame_xpaths:  list
        :param script:       要执行的JavaScript语句
        :type script:        string
        '''
        from chrome_master import util
        frame_id = None
        if frame_xpaths:
            frame_id = self._get_frame_id_by_xpath(frame_xpaths)
        try:
            return self._page_debugger.runtime.eval_script(frame_id, script)
        except util.JavaScriptError as e:
            from qt4w.util import JavaScriptError
            raise JavaScriptError(frame_xpaths, e.message)
        
class ViewPager(ViewGroup):
    '''横向滚动视图
    '''
    @property
    def current_item_index(self):
        '''当前显示项的索引
        '''
        ret = self._driver.get_object_field_value(self.hashcode, 'mCurItem')
        return int(ret)
    
    @property
    def item_count(self):
        '''总项数
        '''
        ret = self._driver.get_object_field_value(self.hashcode, 'mItems.size')
        return int(ret)
    
    def scroll(self, count=1, rect=None):
        '''左右滚动
        
        :param count: 滑动次数，大于0表示向右滑动，小于0表示向左滑动
        :type  count: int
        '''
        if rect == None: rect = self.rect
        x1 = rect[0] + 5
        x2 = rect[0] + rect[2] - 5
        if count > 0:
            # 交换终点
            x = x1
            x1 = x2
            x2 = x
        y = rect[1] + rect[3] // 2
        for _ in range(abs(count)):
            self._driver.drag(x1, y, x2, y, 1)
    
    def __iter__(self):
        '''迭代器
        '''
        for i in range(self.item_count):
            idx = self.current_item_index
            if idx > i:
                # 向右滑动
                self.scroll(i - idx)
            elif idx < i:
                # 向左滑动
                self.scroll(idx - i)
            yield ListItem(self.children[i])
            
class RadioGroup(LinearLayout):
    '''
    '''
    pass

class RecyclerView(AbsListView):
    '''虽然不是继承自ListView，但是可以复用其部分代码，所以从ListView继承过来
    '''
    @func_wrap
    def _update_list(self):
        self._item_count = self._driver.call_object_method(self.hashcode, 'mAdapter', 'getItemCount')
        self._first_visible_position = -1
        self._last_visible_position = -1
        if self._item_count > 0:
            self._children = self.children  # 防止每次都访问
        if len(self._children) > 0:
            # 获取第一个子节点的索引值
            self._first_visible_position = self._get_first_visible_position()
            self._last_visible_position = self._first_visible_position + len(self._children) - 1
    
    def _get_first_visible_position(self):
        '''获取第一个可见节点的位置
        ('mLayoutParams', 'getViewLayoutPosition')
        '''
        if hasattr(self, '_get_first_visible_position_args'):
            return self._driver.call_object_method(self._children[0].hashcode, *self._get_first_visible_position_args)
        
        for args in (('mLayoutParams.mViewHolder', 'getPosition'),
                     ('mLayoutParams.mViewHolder', 'getLayoutPosition')):
            try:
                result = self._driver.call_object_method(self._children[0].hashcode, *args)
                self._get_first_visible_position_args = args
                return result
            except:
                logger.exception('get first visible position by %r failed' % args)
        
    def _is_horizontal(self):
        '''是否横向滑动
        '''
        if not hasattr(self, '__horizontal'):
            if self.item_count >= 2:
                self.__horizontal = self.children[0].rect[0] != self.children[1].rect[0]
            elif self.item_count == 1:
                x_ratio = 1.0 * self.children[0].rect[2] / self.rect[2]
                y_ratio = 1.0 * self.children[0].rect[3] / self.rect[3]
                self.__horizontal = x_ratio < y_ratio
            else:
                return False
        return self.__horizontal
    
    @property
    def reached_left(self):
        '''滑动区域达到最左边
        '''
        for _ in range(3):
            self.update()
            if self.first_position > 0: return False
            if self.item_count == 0: return True
            if len(self._children) == 0: return True
            # 此时第一个子节点肯定可见
            rect = View.rect.fget(self)  # 子类可能会重载rect以修改滑动范围，此时不能根据子类的实现来计算
            try:
                crect = self._children[0].rect
            except RuntimeError as e:
                logger.exception(e)
                continue
            return crect[0] >= rect[0]

    @property
    def reached_right(self):
        '''滑动区域达到最右边
        '''
        if self.item_count == 0: self.update()
        return self.last_position == self.item_count - 1
        
    def scroll_left_one_page(self):
        '''向左滑动一页
        '''
        if self.reached_left: return False
        rect = self.rect
        scroll_x = rect[2]
        item_rect = self._children[0].rect
        if scroll_x > self.first_position * item_rect[2]:
            scroll_x = self.first_position * item_rect[2]
        if scroll_x < 100: scroll_x = 100
        self.scroll(-scroll_x, 0)
        self.on_scroll(-scroll_x, 0)
        return True

    def scroll_right_one_page(self):
        '''向右滑动一页
        '''
        if self.reached_right: return False
        rect = self.rect
        scroll_x = rect[2]
        item_rect = self._children[0].rect
        offset = (self.item_count - self.last_position) * item_rect[2]
        if scroll_x > offset:
            scroll_x = offset
        self.scroll(scroll_x, 0)
        self.on_scroll(scroll_x, 0)
        return True
    
    def scroll_up_one_page(self):
        '''向上滑动一页
        '''
        if self.reached_top: return False
        rect = self.rect
        scroll_y = rect[3]
        child_rect = self._children[0].rect
        if scroll_y > self.first_position * child_rect[3]:
            scroll_y = self.first_position * child_rect[3]
        if scroll_y < child_rect[3]: scroll_y = child_rect[3]
        self.scroll(0, -scroll_y)
        self.on_scroll(0, -scroll_y)
        return True

    def scroll_down_one_page(self):
        '''向下滑动一页
        '''
        if self.reached_bottom: return False
        rect = self.rect
        scroll_y = rect[3]
        child_rect = self._children[0].rect
        offset = (self.item_count - self.last_position) * child_rect[3]
        if scroll_y > offset:
            scroll_y = offset
        elif scroll_y < child_rect[3]:
            scroll_y = child_rect[3]
        self.scroll(0, scroll_y)
        self.on_scroll(0, scroll_y)
        return True
    
    def _scroll_child_to_visible(self, child_idx):
        '''将子节点滚动到可见区域
        '''
        if child_idx < self.first_position:
            # 需要往左/上滑动
            while child_idx < self.first_position:
                if self._is_horizontal():
                    self.scroll_left_one_page()
                else:
                    self.scroll_up_one_page()
                self._update_list()
        elif child_idx > self.last_position:
            # 需要往右/下滑动
            while child_idx > self.last_position:
                if self._is_horizontal():
                    self.scroll_right_one_page()
                else:
                    self.scroll_down_one_page()
                self._update_list()
                
        child = self.children[child_idx - self.first_position]
        self_rect = self.rect
        child_rect = child.rect
        
        # 保证当前项完全可见
        
        if child_rect[0] < self_rect[0]: 
            # 向左滑动
            scroll_x = self_rect[0] - child_rect[0]
            self.scroll(-scroll_x, 0)
        elif child_rect[0] + child_rect[2] > self_rect[0] + self_rect[2]: 
            # 向右滑动
            scroll_x = child_rect[0] + child_rect[2] - (self_rect[0] + self_rect[2])
            self.scroll(scroll_x, 0)
        elif child_rect[1] < self_rect[1]: 
            # 向上滑动
            scroll_y = self_rect[1] - child_rect[1]
            self.scroll(0, -scroll_y)
        elif child_rect[1] + child_rect[3] > self_rect[1] + self_rect[3]:
            # 向下滑动
            scroll_y = child_rect[1] + child_rect[3] - (self_rect[1] + self_rect[3])
            self.scroll(0, scroll_y)
        else:
            return
        self._update_list()
        
    def get_child(self, idx):
        '''提供使用索引访问子元素的方法
        '''
        self.update()
        item_count = self.item_count
        if idx < 0:
            # 允许使用负数索引
            idx += item_count
        if idx < 0 or idx >= item_count:
            raise IndexError('index=%d超出list范围' % idx)
        self._scroll_child_to_visible(idx)
        idx = idx - self.first_position
        if idx < 0 or idx >= len(self._children):
            raise IndexError('index=%d错误，不在范围[0, %d]中' % (idx, len(self._children) - 1))
        return ListItem(self._children[idx], idx)
    
    def __iter__(self):
        '''迭代器
        '''
        self.update()
        for i in range(self.item_count):
            self._scroll_child_to_visible(i)

            idx = i - self.first_position
            if idx >= len(self._children):
                # 可能之前拉到的数据不正确
                self.update()
            if idx < 0 or idx >= len(self._children):
                raise IndexError('%d 不在范围[0, %d]中' % (idx, len(self._children) - 1))
            yield ListItem(self._children[idx])

class DatePicker(FrameLayout):
    '''时间选择器
    '''
    def set_date(self, year, month, day): 
        if year < 0 or month < 1 or month > 12 or day < 1 or day > 31:
            raise RuntimeError('参数传入不符合时间要求')
#         if self._driver._device.sdk_version < 21:
        if self._driver._device.adb.get_sdk_version() < 21:  # 5.0以下用如下方法
            self._driver.call_object_method(self.hashcode, 'mCurrentDate', 'set', 'void', year, month - 1, day)
        else:
            self._driver.call_object_method(self.hashcode, 'mDelegate.mCurrentDate', 'set', 'void', year, month - 1, day)

class ActionMenuItemView(TextView):
    '''android.support.v7.internal.view.menu.ActionMenuItemView
    '''
    pass

class AppCompatEditText(EditText):
    '''android.support.v7.widget.AppCompatEditText
    '''
    pass

class AppCompatImageView(ImageView):
    '''android.support.v7.widget.AppCompatImageView
    '''
    pass

if __name__ == '__main__':
    pass
