# -*- 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 six
from io import BytesIO
import os
import tempfile
import time

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._driver._device.get_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
    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
        :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._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(BytesIO(image_data))
        image = image.crop(self.rect)
        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', 80), self.create_socket) #高版本Chromium内核必须要使用localhost的Host

        if self._pid in self.__class__.debugger_instances and self.__class__.debugger_instances[self._pid]:
            self.__class__.debugger_instances[self._pid].close()
            self.__class__.debugger_instances[self._pid] = None

        self._page_debugger = self.get_debugger()
        self._page_debugger.register_handler(chrome_master.RuntimeHandler)
        self.__class__.debugger_instances[self._pid] = self._page_debugger

    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):
        '''获取当前页面的RemoteDebugger实例
        '''
        url = self.get_page_url()
        return self._chrome_master.find_page(url=url, title=self._title)  # 获取最后一个页面
    
    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 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(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 = 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
