{"action":"created","comment":{"url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/comments/36517702","id":36517702,"diff_hunk":"@@ -0,0 +1,1070 @@\n+# Copyright (c) 2015 Shotgun Software Inc.\n+# \n+# CONFIDENTIAL AND PROPRIETARY\n+# \n+# This work is provided \"AS IS\" and subject to the Shotgun Pipeline Toolkit \n+# Source Code License included in this distribution package. See LICENSE.\n+# By accessing, using, copying or modifying this work you indicate your \n+# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights \n+# not expressly granted therein are reserved by Shotgun Software Inc.\n+\n+\"\"\"\n+Custom Qt View that displays items as a grouped list.  The view works with any tree\n+based model with the first level of the hierarchy defining the groups and the second\n+level defining the items for that group.  Subsequent levels of the hierarchy are ignored.\n+\n+Items within a group are layed out left-to right and wrap automatically based on the\n+view's width.\n+\n+For example, the following tree model:\n+\n+- Group 1\n+  - Item 1\n+  - Item 2\n+  - Item 3\n+- Group 2\n+  - Item 4\n+- Group 3\n+\n+Would look like this in the view:\n+\n+> Group 1\n+-----------------\n+[Item 1] [Item 2]\n+[Item 3]\n+> Group 2\n+-----------------\n+[Item 4]\n+> Group 3\n+-----------------\n+\n+The widgets used for the various groups and items are created through a GroupedListViewItemDelegate\n+and this can be overriden to implement custom UI for these elements.\n+\"\"\"\n+\n+import sgtk\n+from sgtk.platform.qt import QtCore, QtGui\n+\n+from .grouped_list_view_item_delegate import GroupedListViewItemDelegate\n+\n+class GroupedListView(QtGui.QAbstractItemView):\n+    \"\"\"\n+    The main grouped list view class\n+    \"\"\"\n+\n+    class _ItemInfo(object):\n+        \"\"\"\n+        class representing the information that needs to be tracked for each item (row)\n+        in the model.\n+        \"\"\"\n+        def __init__(self):\n+            \"\"\"\n+            Construction\n+            \"\"\"\n+            self.rect = QtCore.QRect()             # relative item rect for group header\n+            self.dirty = True                      # True if data in group or children has changed\n+            self.collapsed = False                 # True if the group is currently collapsed\n+            self.child_info = []                   # List of tuples containing (row, column, size) where row and\n+                                                   # column are the relative row and column in the visual grid and \n+                                                   # size is the visual size of the child for all child items in\n+                                                   # the group\n+            self.child_area_rect = QtCore.QRect()  # total size of child area\n+\n+        def __repr__(self):\n+            return \"%s: %s\" % (self.rect, self.child_area_rect)\n+\n+    def __init__(self, parent):\n+        \"\"\"\n+        Construction\n+\n+        :param parent:    The parent QWidget\n+        \"\"\"\n+        QtGui.QAbstractItemView.__init__(self, parent)\n+\n+        self._item_info = []\n+        self._update_all_item_info = True\n+        self._update_some_item_info = False\n+        self._max_width = 0\n+\n+        # keep track of all created group widgets.  The view\n+        # will always try to reuse these where possible so the\n+        # number of created group widgets will never exceed\n+        # the number visible in the viewport  \n+        self._group_widgets = []\n+        self._group_widget_rows = {}\n+\n+        self._prev_viewport_sz = QtCore.QSize()\n+\n+        # initial values for the properties\n+        self._border = QtCore.QSize(6,6)\n+        self._group_spacing = 30\n+        self._item_spacing = QtCore.QSize(4,4)\n+\n+        # default item delegate:\n+        self._default_item_delegate = GroupedListViewItemDelegate(self)\n+\n+    # @property\n+    def _get_border(self):\n+        \"\"\"\n+        The external border to use for all items in the view\n+        \"\"\"\n+        return self._border\n+    # @border.setter\n+    def _set_border(self, border_sz):\n+        self._border = border_sz\n+        self._update_all_item_info = True\n+        self.viewport().update()\n+    border = property(_get_border, _set_border)\n+\n+    # @property\n+    def _get_group_spacing(self):\n+        \"\"\"\n+        The spacing to use between groups when they are collapsed\n+        \"\"\"\n+        return self._group_spacing\n+    # @group_spacing.setter\n+    def _set_group_spacing(self, spacing):\n+        self._group_spacing = spacing\n+        self._update_all_item_info = True\n+        self.viewport().update()\n+    group_spacing = property(_get_group_spacing, _set_group_spacing)\n+\n+    # @property\n+    def _get_item_spacing(self):\n+        \"\"\"\n+        The spacing to use between items in the view\n+        \"\"\"\n+        return self._item_spacing\n+    # @item_spacing.setter\n+    def _set_item_spacing(self, spacing):\n+        self._item_spacing = spacing\n+        self._update_all_item_info = True\n+        self.viewport().update()\n+    item_spacing = property(_get_item_spacing, _set_item_spacing)\n+\n+    def expand(self, index):\n+        \"\"\"\n+        Expand the specified index\n+\n+        :param index:   The model index to be expanded\n+        \"\"\"\n+        self._set_expanded(index, True)\n+\n+    def collapse(self, index):\n+        \"\"\"\n+        Collapse the specified index\n+\n+        :param index:   The model index to be collapsed\n+        \"\"\"\n+        self._set_expanded(index, False)\n+\n+    def is_expanded(self, index):\n+        \"\"\"\n+        Query if the specified index is expanded or not\n+\n+        :param index:   The model index to check\n+        :returns:       True if the index is a root index and is expanded,\n+                        otherwise False\n+        \"\"\"\n+        if not index.isValid() or index.parent() != self.rootIndex():\n+            return False\n+        row = index.row()\n+        if row < len(self._item_info):\n+            return not self._item_info[row].collapsed\n+        else:\n+            return False\n+\n+    # ----------------------------------------------------------------------------------------------------\n+    # Overriden public methods\n+\n+    def itemDelegate(self):\n+        \"\"\"\n+        Overriden base method that returns the item delegate to be used for items in this\n+        view.  This will return the default item delegate if a delegate deriving from\n+        GroupedListViewItemDelegate hasn't been set for the view.\n+\n+        :returns:   A GroupedListViewItemDelegate instance\n+        \"\"\"\n+        delegate = QtGui.QAbstractItemView.itemDelegate(self)\n+        if not isinstance(delegate, GroupedListViewItemDelegate):\n+            # just return the defauly item delegate:\n+            delegate = self._default_item_delegate\n+        return delegate\n+\n+    def edit(self, idx, trigger, event):\n+        \"\"\"\n+        Override the edit method on the base class to dissalow editing \n+        of group items\n+\n+        :param idx:     The model index to be edited\n+        :param trigger: The trigger that is triggering the edit\n+        :param event:   The edit event\n+        :returns:       False if the idx is a root item (group), otherwise\n+                        the returned value from the base implementation\n+        \"\"\"\n+        if idx.parent() == self.rootIndex():\n+            # we don't want to allow the regular editing of groups\n+            # needs to be driven by role though!\n+            return False\n+        return QtGui.QAbstractItemView.edit(self, idx, trigger, event)\n+\n+    def setModel(self, model):\n+        \"\"\"\n+        Overrides the base method to make sure the item info gets updated when the model\n+        is changed.\n+\n+        :param model:   The model to set for this view\n+        \"\"\"\n+        self._update_all_item_info = True\n+        QtGui.QAbstractItemView.setModel(self, model)\n+\n+    # ----------------------------------------------------------------------------------------------------\n+    # Internal class methods\n+\n+    def _set_expanded(self, index, expand):\n+        \"\"\"\n+        Toggle the expanded state of the specified index\n+\n+        :param index:   The model index to expand/collapse\n+        :param expand:  True if the item should be expanded, otherwise False\n+        \"\"\"\n+        if not index.isValid() or index.parent != self.rootIndex():\n+            # can only expand valid root indexes!\n+            return \n+\n+        row = index.row()\n+        if row < len(self._item_info):\n+            self._item_info[row].collapsed = not expand\n+            self._item_info[row].dirty = True\n+            self._update_some_item_info = True\n+            self.viewport().update()\n+\n+    def dataChanged(self, top_left, bottom_right):\n+        \"\"\"\n+        Overriden base class method that gets called when some data in the model attached\n+        to this view has been changed.\n+\n+        :param top_left:        The top-left model index of the data that has changed\n+        :param bottom_right:    The bottom-right model index of the data that has changed\n+        \"\"\"\n+        #print \"DATA CHANGED [%s] %s -> %s\" % (top_left.parent().row(), top_left.row(), bottom_right.row())\n+\n+        if top_left.parent() == self.rootIndex():\n+            # data has changed for top-level rows:\n+            for row in range(top_left.row(), bottom_right.row()+1):\n+                if row < len(self._item_info):\n+                    self._item_info[row].dirty = True\n+                self._update_some_item_info = True\n+        elif top_left.parent().parent() == self.rootIndex():\n+            # this assumes that all rows from top-left to bottom-right have \n+            # the same parent - this seems to always be the case!\n+            row = top_left.parent().row()\n+            if row < len(self._item_info):\n+                self._item_info[row].dirty = True\n+            self._update_some_item_info = True\n+        else:\n+            self._update_all_item_info = True\n+\n+        # make sure we schedule a viewport update so that everything gets updated correctly!\n+        self.viewport().update()\n+        QtGui.QAbstractItemView.dataChanged(self, top_left, bottom_right)\n+\n+    def rowsInserted(self, parent_index, start, end):\n+        \"\"\"\n+        Overriden base method that gets called when new rows have been inserted into\n+        the model attached to this view.\n+\n+        :param parent_index:    The parent model index the rows have been inserted under\n+        :param start:           The first row that was inserted\n+        :param end:             The last row that was inserted\n+        \"\"\"\n+        #print \"ROWS INSERTED [%s] %s -> %s\" % (parent_index.row(), start, end)\n+\n+        if not self._update_all_item_info:\n+            if parent_index == self.rootIndex():\n+                # inserting root level rows:\n+                new_rows = [GroupedListView._ItemInfo() for x in range(end+1-start)]\n+                self._item_info = self._item_info[:start] + new_rows + self._item_info[start:]\n+                self._update_some_item_info = True\n+            elif parent_index.parent() == self.rootIndex():\n+                # inserting group level rows:\n+                parent_row = parent_index.row()\n+                if parent_row < len(self._item_info):\n+                    self._item_info[parent_row].dirty = True\n+                    self._update_some_item_info = True\n+                else:\n+                    self._update_all_item_info = True\n+            else:\n+                # something went wrong so refresh everything!\n+                self._update_all_item_info = True\n+\n+        # make sure we schedule a viewport update so that everything gets updated correctly!\n+        self.viewport().update()\n+        QtGui.QAbstractItemView.rowsInserted(self, parent_index, start, end)\n+\n+    def rowsAboutToBeRemoved(self, parent_index, start, end):\n+        \"\"\"\n+        Overriden base method that gets called just before rows are removed from\n+        the model attached to this view.\n+\n+        Note, not sure why but this doesn't seem to get called as expected in PyQt!  Because\n+        of this there is an extra validation step in self._update_item_info() which may\n+        slightly reduce performance in PyQt but as this only happens when items are removed\n+        from the model via clearing then hopefully it shouldn't be a big problem!\n+\n+        :param parent_index:    The parent model index the rows have been inserted under\n+        :param start:           The first row that will be removed\n+        :param end:             The last row that will be removed\n+        \"\"\"\n+        #print \"ROWS REMOVED [%s] %s -> %s\" % (parent_index.row(), start, end)\n+\n+        if not self._update_all_item_info:\n+            if parent_index == self.rootIndex():\n+                # removing root level rows:\n+                self._item_info = self._item_info[:start] + self._item_info[end+1:]\n+                self._update_some_item_info = True\n+            elif parent_index.parent() == self.rootIndex():\n+                # inserting group level rows:\n+                parent_row = parent_index.row()\n+                if parent_row < len(self._item_info):\n+                    self._item_info[parent_row].dirty = True\n+                    self._update_some_item_info = True\n+                else:\n+                    self._update_all_item_info = True\n+            else:\n+                # something went wrong!\n+                self._update_all_item_info = True        \n+\n+        QtGui.QAbstractItemView.rowsAboutToBeRemoved(self, parent_index, start, end)\n+\n+        # make sure we schedule a viewport update so that everything gets updated correctly!\n+        self.viewport().update()\n+\n+    def visualRect(self, index):\n+        \"\"\"\n+        Overriden base method that should return the rectangle occupied by the given \n+        index in the viewport\n+\n+        :param index:   The model index to return the rectangle for\n+        :returns:       A QRect() representing the rectangle this index will occupy \n+                        in the viewport\n+        \"\"\"\n+        rect = QtCore.QRect()\n+        if index.isValid():\n+            item_rect = self._get_item_rect(index)\n+            if item_rect.isValid():\n+                # rectangle should be widget relative, not viewport relative:\n+                rect = item_rect.translated(-self.horizontalOffset(), -self.verticalOffset())\n+        return rect\n+\n+    def isIndexHidden(self, index):\n+        \"\"\"\n+        Overriden base method that returns True if the specified index is hidden (e.g. a \n+        collapsed child in a tree view)\n+\n+        :param index:   The model index to query if it's hidden\n+        :returns:       True if the index is hidden, False otherwise\n+        \"\"\"\n+        if not index.isValid():\n+            return False\n+\n+        parent_index = index.parent() \n+        if parent_index == self.rootIndex():\n+            # root items are never hidden:\n+            return False\n+\n+        if parent_index.parent() != self.rootIndex():\n+            # grandchildren are always hidden:\n+            return True\n+\n+        row = parent_index.row()\n+        if row < len(self._item_info):\n+            if self._item_info[row].collapsed:\n+                # parent is collapsed so item is hidden\n+                return True\n+\n+        # default is to show the index:\n+        return False\n+\n+    def scrollTo(self, index, scroll_hint=QtGui.QAbstractItemView.EnsureVisible):\n+        \"\"\"\n+        Overriden base method used to scroll to the specified index in the viewport\n+        (TODO - implement behaviour specific to the scroll hint)\n+\n+        :param index:       The model index to scroll to\n+        :param scroll_hint: Hint about how the view should scroll - currently ignored!\n+        \"\"\"\n+        viewport_rect = self.viewport().rect()\n+\n+        item_rect = self._get_item_rect(index)\n+        item_rect = item_rect.translated(-self.horizontalOffset(), -self.verticalOffset())\n+\n+        dx = 0\n+        if item_rect.left() < viewport_rect.left():\n+            dx = item_rect.left() - viewport_rect.left()\n+        elif item_rect.right() > viewport_rect.right():\n+            dx = min(item_rect.right() - viewport_rect.right(),\n+                     item_rect.left() - viewport_rect.left())\n+        if dx != 0:\n+            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + dx)\n+\n+        dy = 0\n+        if item_rect.top() < viewport_rect.top():\n+            dy = item_rect.top() - viewport_rect.top()\n+        elif item_rect.bottom() > viewport_rect.bottom():\n+            dy = min(item_rect.bottom() - viewport_rect.bottom(),\n+                     item_rect.top() - viewport_rect.top())\n+        if dy != 0:\n+            self.verticalScrollBar().setValue(self.verticalScrollBar().value() + dy)\n+\n+        # update the viewport\n+        self.viewport().update()\n+\n+    def indexAt(self, point):\n+        \"\"\"\n+        Overriden base method that returns the model index under the specified point in \n+        the viewport\n+\n+        :param point:   The QPoint to find the model index for\n+        :returns:       The model index for the item under the point or an invalid\n+                        QModelIndex() if there isn't one.\n+        \"\"\"\n+        if not self.model():\n+            return QtCore.QModelIndex()\n+\n+        # convert viewport relative point to global point:\n+        point = point + QtCore.QPoint(self.horizontalOffset(), self.verticalOffset())\n+\n+        num_rows = len(self._item_info)\n+        if num_rows != self.model().rowCount():\n+            # just in case!\n+            return QtCore.QModelIndex()\n+\n+        y_offset = self._border.height()\n+        for row, item_info in enumerate(self._item_info):\n+\n+            # get point in local space:\n+            local_point = point + QtCore.QPoint(0, -y_offset)\n+\n+            if local_point.y() < item_info.rect.y():\n+                # point definitely isn't on an item as we'd have found it by now!\n+                break\n+\n+            # ok, we'll need an index for this row:\n+            index = self.model().index(row, 0)\n+\n+            # check if the point is within this item:\n+            if item_info.rect.contains(local_point):\n+                return index \n+\n+            # update y-offset:\n+            y_offset += item_info.rect.height()\n+\n+            if not item_info.collapsed:\n+                # now check children:\n+                local_point = point + QtCore.QPoint(0, -y_offset)\n+                for child_row, (_, _, child_rect) in enumerate(item_info.child_info):\n+                    if child_rect.contains(local_point):\n+                        # found a hit on a child item\n+                        return self.model().index(child_row, 0, index)\n+\n+                # update y-offset                \n+                y_offset += item_info.child_area_rect.height() + self._group_spacing\n+            else:\n+                y_offset += self._item_spacing.height()\n+\n+        # no match so return invalid model index\n+        return QtCore.QModelIndex()\n+\n+    def moveCursor(self, cursor_action, keyboard_modifiers):\n+        \"\"\"\n+        Overriden base method that returns the index for the item that the specified \n+        cursor action will move to.\n+\n+        Currently handles up, down, left, right, next and previous actions and only\n+        moves between leaf items in the view, skipping group items.  This could be\n+        extended in future to treat groups in the same way the regular tree view works\n+        (e.g. left/right == collapes/expand).\n+\n+        :param cursor_action:       The action to use when moving the cursor.\n+        :param keyboard_modifiers:  Any keyboard modifiers that are currentlt active.\n+        :returns:                   The QModelIndex of the item the cursor should be\n+                                    moved to.\n+        \"\"\"\n+        index = self.currentIndex()\n+\n+        # check that the item info is up-to-date:\n+        if len(self._item_info) != self.model().rowCount():\n+            return index\n+\n+        # get the rows for both the index and the parent index:\n+        row = index.row()\n+        parent_index = index.parent()\n+        if not parent_index.isValid() or parent_index.parent() != self.rootIndex():\n+            return index\n+        parent_row = parent_index.row()\n+\n+        parent_item_info = self._item_info[parent_row]\n+        if len(parent_item_info.child_info) != self.model().rowCount(parent_index):\n+            return index\n+\n+        # find the neighbouring row information based on the direction:\n+        if (cursor_action == QtGui.QAbstractItemView.MoveLeft\n+            or cursor_action == QtGui.QAbstractItemView.MovePrevious):\n+            # move to the left of the current item in the same visual row if possible.  If the\n+            # cursor is at the start of the row then we do nothing.\n+            if (row < len(parent_item_info.child_info)\n+                and parent_item_info.child_info[row][0] == parent_item_info.child_info[row-1][0]):\n+                # just move left 1\n+                row -= 1\n+        elif (cursor_action == QtGui.QAbstractItemView.MoveRight\n+              or cursor_action == QtGui.QAbstractItemView.MoveNext):\n+            # move to the right of the current item in the same visual row if possible.  If the\n+            # cursor is at the end of the row then we do nothing.\n+            if ((row + 1) < len(self._item_info[parent_row].child_info)\n+                and parent_item_info.child_info[row][0] == parent_item_info.child_info[row+1][0]):\n+                # just move right 1:\n+                row += 1\n+        elif cursor_action == QtGui.QAbstractItemView.MoveUp:\n+            # move to the same column in the visible visual row above the current item if possible.  If we\n+            # are on the top visible row in the view then this will do nothing.\n+            layout_row, layout_column, _ = parent_item_info.child_info[row]\n+            if layout_row > 0:\n+                # move up a row within the same group:\n+                for ri, (r, c, _) in enumerate(reversed(parent_item_info.child_info[:row])):\n+                    if r == layout_row-1 and c <= layout_column:\n+                        row = row-1-ri\n+                        break\n+            else:\n+                # see if we have a visible previous group we can move up to:\n+                prev_item_info = None\n+                prev_parent_row = 0\n+                for ii, info in enumerate(reversed(self._item_info[:parent_row])):\n+                    if not info.collapsed and info.child_info:\n+                        # we can move up to this group!\n+                        prev_item_info = info\n+                        prev_parent_row = parent_row-1-ii\n+                        break\n+                if prev_item_info:\n+                    # try to move up to the last row of the previous group:\n+                    for ri, (r, c, _) in enumerate(reversed(prev_item_info.child_info)):\n+                        if (r == 0 and c <= layout_column) or (r > 0 and c == layout_column):\n+                            parent_row = prev_parent_row\n+                            row = len(prev_item_info.child_info)-1-ri\n+                            break\n+\n+        elif cursor_action == QtGui.QAbstractItemView.MoveDown:\n+            # move to the same column in the visible visual row below the current item if possible.  If we\n+            # are on the bottom visible row in the view then this will do nothing.\n+            layout_row, layout_column, _ = parent_item_info.child_info[row]\n+            last_layout_row, last_layout_column, _ = parent_item_info.child_info[-1]\n+            if layout_row < last_layout_row:\n+                # move down a row within the same group:\n+                for ri, (r, c, _) in enumerate(parent_item_info.child_info[row:]):\n+                    if ((r == last_layout_row and c == min(layout_column, last_layout_column))\n+                        or r > layout_row and c == layout_column):\n+                        row = row + ri\n+                        break\n+            else:\n+                # see if we have a visible following group we can move down to:\n+                next_item_info = None\n+                next_parent_row = 0\n+                for ii, info in enumerate(self._item_info[parent_row+1:]):\n+                    if not info.collapsed and info.child_info:\n+                        # we can move down to this group!\n+                        next_item_info = info\n+                        next_parent_row = parent_row + 1 + ii\n+                        break\n+                if next_item_info:\n+                    last_layout_row, last_layout_column, _ = next_item_info.child_info[-1]\n+                    # try to move down to the first row of the next group:\n+                    for ri, (r, c, _) in enumerate(next_item_info.child_info):\n+                        if ((r == last_layout_row and c == min(layout_column, last_layout_column))\n+                            or c == layout_column):\n+                            parent_row = next_parent_row\n+                            row = ri\n+                            break\n+\n+        # build a new index based on the new row and parent row:\n+        if parent_row >= 0 and parent_row < self.model().rowCount():\n+            parent_index = self.model().index(parent_row, 0)\n+            if row >= 0 and row < self.model().rowCount(parent_index):\n+                index = self.model().index(row, 0, parent_index)\n+\n+        return index\n+\n+\n+    def horizontalOffset(self):\n+        \"\"\"\n+        Overriden base method that returns the X offset of the viewport within the ideal \n+        sized widget\n+\n+        :returns:   The current x-offset based on the horizontal scroll bar value\n+        \"\"\"\n+        return self.horizontalScrollBar().value()\n+\n+    def verticalOffset(self):\n+        \"\"\"\n+        Overriden base method that returns the Y offset of the viewport within the ideal \n+        sized widget\n+\n+        :returns:   The current y-offset based on the vertical scroll bar value\n+        \"\"\"\n+        return self.verticalScrollBar().value()    \n+\n+    def scrollContentsBy(self, dx, dy):\n+        \"\"\"\n+        Overriden base method used to scroll the viewport by the specified deltas\n+\n+        :param dx:  The horizontal delta to scroll by\n+        :param dy:  The vertical delta to scroll by\n+        \"\"\"\n+        self.scrollDirtyRegion(dx, dy)\n+        self.viewport().scroll(dx, dy)\n+        self.viewport().update()\n+\n+    def setSelection(self, selection_rect, flags):\n+        \"\"\"\n+        Overriden base method used to set the selection given the selection rectangle and flags\n+\n+        :param selection_rect:  The selection rectangle that should be used to select any\n+                                items contained within\n+        :param flags:           Flags that define if the items within the selection rectangle\n+                                should be added to, removed from, etc. the current selection\n+        \"\"\"\n+        # convert viewport relative rect to global rect:\n+        selection_rect = selection_rect.translated(self.horizontalOffset(), self.verticalOffset())\n+\n+        # now find which item rectangles intersect this rectangle:\n+        selection = QtGui.QItemSelection()\n+\n+        num_rows = len(self._item_info)\n+        if num_rows != self.model().rowCount():\n+            # just in case!\n+            return\n+\n+        y_offset = self._border.height()\n+        for row, item_info in enumerate(self._item_info):\n+\n+            # we only allow selection of child items so we can skip testing the group/top level:\n+            y_offset += item_info.rect.height()\n+\n+            if not item_info.collapsed:\n+                # check to see if the selection rect intersects the child area:\n+                local_selection_rect = selection_rect.translated(0, -y_offset)\n+\n+                if local_selection_rect.intersects(item_info.child_area_rect):\n+                    # we'll need an index for this row:\n+                    index = self.model().index(row, 0)\n+\n+                    # need to iterate through and check all child items:\n+                    top_left = bottom_right = None\n+                    for child_row, (_, _, child_rect) in enumerate(item_info.child_info):\n+\n+                        if child_rect.intersects(local_selection_rect):\n+                            child_index = self.model().index(child_row, 0, index)\n+                            top_left = top_left or child_index\n+                            bottom_right = child_index\n+                        else:\n+                            if top_left:\n+                                selection.select(top_left, bottom_right)\n+                                top_left = bottom_right = None\n+\n+                    if top_left:\n+                        selection.select(top_left, bottom_right)\n+                elif local_selection_rect.bottom() > item_info.child_area_rect.top():\n+                    # no need to look any further!\n+                    pass\n+\n+                # update y-offset\n+                y_offset += item_info.child_area_rect.height() + self._group_spacing\n+            else:\n+                y_offset += self._item_spacing.height()\n+\n+        # update the selection model:\n+        self.selectionModel().select(selection, flags)\n+\n+    def visualRegionForSelection(self, selection):\n+        \"\"\"\n+        Overriden base method that returns the region in the viewport encompasing all the \n+        selected items\n+\n+        :param selection:   The selection to return the region for\n+        :returns:           A QRegion representing the region containing all the selected items\n+        \"\"\"\n+        viewport_offset = (-self.horizontalOffset(), -self.verticalOffset())        \n+\n+        region = QtGui.QRegion()\n+        for index_range in selection:\n+            for row in range(index_range.top(), index_range.bottom()+1):\n+                index = self.model().index(row, 0, index_range.parent())\n+                rect = self._get_item_rect(index)\n+                rect = rect.translated(viewport_offset[0], viewport_offset[1])\n+                region += rect\n+        return region\n+\n+    def paintEvent(self, event):\n+        \"\"\"\n+        Overriden base method that gets called whenever the view needs repainting.\n+\n+        :param event:    The QPaintEvent containing information about the event\n+        \"\"\"\n+        if not self.model():\n+            return\n+\n+        # make sure item rects are up to date:\n+        self._update_item_info()\n+\n+        row_count = self.model().rowCount()\n+        if row_count != len(self._item_info):\n+            # this shouldn't ever happen but just incase it does then \n+            # we shouldn't paint anything as we'll probably get exceptions!\n+            bundle = sgtk.platform.current_bundle()\n+            bundle.log_warning(\"Unable to paint the Grouped List View as the internal cache is out of sync!\")\n+            return\n+\n+        # build lookups for the group widgets:\n+        group_widgets_by_row = {}\n+        for widget, row in self._group_widget_rows.iteritems():\n+            if row < row_count:\n+                group_widgets_by_row[row] = widget\n+        unused_group_widgets = []\n+        for widget in self._group_widgets:\n+            if widget not in group_widgets_by_row.values():\n+                unused_group_widgets.append(widget)\n+        next_unused_group_widget_idx = 0\n+        self._group_widget_rows = {}\n+        group_widgets_to_resize = []\n+\n+        # pull out the viewport size and offsets:\n+        update_rect = event.rect()\n+        viewport_rect = self.viewport().rect()\n+        viewport_offset = (-self.horizontalOffset(), -self.verticalOffset())\n+\n+        # start painting:\n+        painter = QtGui.QPainter(self.viewport())\n+        try:\n+            painter.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.TextAntialiasing)\n+\n+            # keep track of the y-offset as we go:\n+            y_offset = self._border.height()\n+            for row, item_info in enumerate(self._item_info):\n+\n+                # get valid model index:\n+                index = self.model().index(row, 0, self.rootIndex())\n+\n+                # get the rectangle and translate into the correct relative location:\n+                rect = item_info.rect.translated(viewport_offset[0], viewport_offset[1] + y_offset)\n+\n+                # test to see if the rectangle exists within the viewport:\n+                grp_widget = group_widgets_by_row.get(row)\n+                if rect.isValid and rect.intersects(viewport_rect):\n+                    # the group widget is visible:\n+                    if not grp_widget:\n+                        if next_unused_group_widget_idx < len(unused_group_widgets):\n+                            grp_widget = unused_group_widgets[next_unused_group_widget_idx]\n+                            next_unused_group_widget_idx += 1\n+                        else:\n+                            # need to create a new group widget and hook up the signals:\n+                            if hasattr(self.itemDelegate(), \"create_group_widget\"):\n+                                grp_widget = self.itemDelegate().create_group_widget(self.viewport())\n+                            if grp_widget:\n+                                self._group_widgets.append(grp_widget)\n+                                grp_widget.toggle_expanded.connect(self._on_group_expanded_toggled)\n+\n+                    if grp_widget:\n+                        if grp_widget.geometry() != rect:\n+                            # add widget to list to be resized later:\n+                            group_widgets_to_resize.append((grp_widget, rect))\n+\n+                        # set up this widget for this index:\n+                        grp_widget.set_expanded(not item_info.collapsed)\n+                        grp_widget.set_item(index)\n+                        grp_widget.show()\n+                        self._group_widget_rows[grp_widget] = row\n+\n+                elif grp_widget:\n+                    # group widget is hidden!\n+                    unused_group_widgets.append(grp_widget)\n+\n+                # add the group rectangle height to the y-offset\n+                y_offset += rect.height()\n+\n+                if not item_info.collapsed:\n+                    # draw any children:\n+                    num_child_rows = self.model().rowCount(index)\n+                    if len(item_info.child_info) == num_child_rows:\n+                        # draw all children\n+                        for child_row, (_, _, child_rect) in enumerate(item_info.child_info):\n+                            # figure out index and update rect:\n+                            child_index = self.model().index(child_row, 0, index)\n+\n+                            child_rect = child_rect.translated(viewport_offset[0], viewport_offset[1] + y_offset)\n+                            if not child_rect.isValid or not child_rect.intersects(update_rect):\n+                                # no need to draw!\n+                                continue\n+\n+                            # set up the rendering options:\n+                            # option = self.viewOptions())\n+                            # (AD) - using self.viewOptions() to get the view style options seems\n+                            # to return an invalid item in some versions of PySide/PyQt!  I think\n+                            # it's returning a QtGui.QStyleOptionViewItem even though the \n+                            # underlying C++ object is a QtGui.QStyleOptionViewItemV2 or higher.\n+                            #\n+                            # This would result in option.rect being corrupt immediately after it\n+                            # was set below!\n+                            #\n+                            # creating the object directly and then using initFrom seems to work\n+                            # though.\n+                            option = QtGui.QStyleOptionViewItem()\n+                            option.initFrom(self)\n+\n+                            option.rect = child_rect\n+\n+                            if self.selectionModel().isSelected(child_index):\n+                                option.state |= QtGui.QStyle.State_Selected\n+                            if child_index == self.currentIndex():\n+                                option.state |= QtGui.QStyle.State_HasFocus\n+\n+                            # draw the widget using the item delegate\n+                            self.itemDelegate().paint(painter, option, child_index)\n+\n+                    # update the y-offset to include the child area:\n+                    y_offset += item_info.child_area_rect.height() + self._group_spacing\n+                else:\n+                    y_offset += self._item_spacing.height()\n+\n+            # hide any group widgets that were not used:\n+            for w in unused_group_widgets[next_unused_group_widget_idx:]:\n+                if w.isVisible():\n+                    w.hide()\n+        finally:\n+            painter.end()\n+\n+        # update geometry for any group widgets that need updating\n+        # Note, this has to be done after painting has finished otherwise\n+        # the resize event gets blocked!\n+        for widget, rect in group_widgets_to_resize:\n+            widget.setGeometry(rect)\n+\n+        # call the base implementation:\n+        QtGui.QAbstractItemView.paintEvent(self, event)\n+\n+    def _on_group_expanded_toggled(self, expanded):\n+        \"\"\"\n+        Slot that gets signalled whenever a group widget is expanded/collapsed.\n+\n+        :param expanded:    True if the group widget was expanded, False if it was \n+                            collapsed\n+        \"\"\"\n+        # get the row that is being expanded:\n+        group_widget = self.sender()\n+        row = self._group_widget_rows.get(group_widget)\n+        if row == None or row >= len(self._item_info):\n+            return\n+\n+        # toggle collapsed state for item:        \n+        self._item_info[row].collapsed = not expanded\n+        self._item_info[row].dirty = True\n+        self._update_some_item_info = True\n+\n+        # and update the viewport:\n+        self.viewport().update()\n+\n+    def updateGeometries(self):\n+        \"\"\"\n+        Overriden base method responsible for updating the horizontal and vertical scroll \n+        bars so that they will correctly scroll the view's viewport.\n+        \"\"\"\n+        # calculate the maximum height of all visible items in the model:\n+        max_height = 0\n+        for item_info in self._item_info:\n+            max_height += item_info.rect.height() + self._group_spacing\n+            if not item_info.collapsed:\n+                max_height += item_info.child_area_rect.height()\n+\n+        self.horizontalScrollBar().setSingleStep(30)\n+        self.horizontalScrollBar().setPageStep(self.viewport().width())\n+        self.horizontalScrollBar().setRange(0, max(0, self._max_width - self.viewport().width()))\n+\n+        self.verticalScrollBar().setSingleStep(30)#00)# TODO - make this more intelligent!\n+        self.verticalScrollBar().setPageStep(self.viewport().height())\n+        self.verticalScrollBar().setRange(0, max(0, max_height - self.viewport().height()))\n+\n+    def _get_item_rect(self, index):\n+        \"\"\"\n+        Return the cached item rectangle for the specified model index.\n+\n+        :param index:   The model index to find the item rectangle for\n+        :returns:       A QRect representing the rectangle this index occupies in \n+                        the view.  This rectangle is viewport relative.\n+        \"\"\"\n+        # first, get the row for each level of the hierarchy (bottom to top)\n+        rows = []\n+        while index.isValid() and index != self.rootIndex():\n+            rows.append(index.row())\n+            index = index.parent()\n+\n+        # find the info for the root item:\n+        root_row = rows[-1]\n+        if root_row >= len(self._item_info):\n+            return QtCore.QRect()\n+        root_info = self._item_info[root_row]\n+\n+        # and the Y offset for the start of the root item:\n+        y_offset = self._border.height()\n+        for row_info in self._item_info[:root_row]:\n+            y_offset += row_info.rect.height()\n+            if not row_info.collapsed:\n+                y_offset += row_info.child_area_rect.height()\n+                y_offset += self._group_spacing\n+            else:\n+                y_offset += self._item_spacing.height()\n+\n+        # get the rect for the leaf item:\n+        rect = QtCore.QRect()\n+        if len(rows) == 1:\n+            rect = root_info.rect\n+        else:\n+            y_offset += root_info.rect.height()\n+            child_row = rows[-2]\n+            if child_row < len(root_info.child_info):\n+                rect = self._item_info[root_row].child_info[child_row][2]\n+\n+        # and offset the rect by the Y offset:\n+        rect = rect.translated(0, y_offset)\n+\n+        return rect\n+\n+    def _update_item_info(self):\n+        \"\"\"\n+        Update the cached item info when needed.  This updates the item layout for any items that have\n+        been 'dirtied' or if the widget size has changed, etc.\n+\n+        This is typically run immediately before painting.\n+        \"\"\"\n+        # double check that the item-info list is the correct length.  PyQt doesn't\n+        # seem to call 'rowsAboutToBeRemoved' when a model is cleared so this list can \n+        # become out of sync!\n+        if self.model().rowCount() != len(self._item_info):\n+            self._update_all_item_info = True\n+\n+        # check to see if the viewport size has changed:\n+        viewport_sz = self.viewport().size()\n+        viewport_resized = False\n+        if not self.verticalScrollBar().isVisible():\n+            # to avoid unnecessary resizing, we always calculate the viewport width as if\n+            # the vertical scroll bar were visible:\n+            scroll_bar_width = self.style().pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)\n+            viewport_sz.setWidth(viewport_sz.width() - scroll_bar_width)\n+\n+        if (viewport_sz != self._prev_viewport_sz):\n+            # the viewport width has changed so we'll need to update all geometry :(\n+            viewport_resized = True\n+            # keep track of the new viewport size for the next time\n+            self._prev_viewport_sz = viewport_sz \n+\n+        if not self._update_some_item_info and not self._update_all_item_info and not viewport_resized:\n+            # nothing to do!\n+            return\n+\n+        #print \"%s, %s, %s, %s\" % (self._update_all_item_info, self._update_some_item_info, viewport_resized, self._item_info)\n+        self._update_all_item_info = self._update_all_item_info or viewport_resized\n+\n+        # (AD) TEMP","path":"python/views/grouped_list_view/grouped_list_view.py","position":974,"original_position":974,"commit_id":"d5b57274d688343691dffff4a526bcce669f65bc","original_commit_id":"d5b57274d688343691dffff4a526bcce669f65bc","user":{"login":"jfboismenu-adsk","id":8126447,"avatar_url":"https://avatars.githubusercontent.com/u/8126447?v=3","gravatar_id":"","url":"https://api.github.com/users/jfboismenu-adsk","html_url":"https://github.com/jfboismenu-adsk","followers_url":"https://api.github.com/users/jfboismenu-adsk/followers","following_url":"https://api.github.com/users/jfboismenu-adsk/following{/other_user}","gists_url":"https://api.github.com/users/jfboismenu-adsk/gists{/gist_id}","starred_url":"https://api.github.com/users/jfboismenu-adsk/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jfboismenu-adsk/subscriptions","organizations_url":"https://api.github.com/users/jfboismenu-adsk/orgs","repos_url":"https://api.github.com/users/jfboismenu-adsk/repos","events_url":"https://api.github.com/users/jfboismenu-adsk/events{/privacy}","received_events_url":"https://api.github.com/users/jfboismenu-adsk/received_events","type":"User","site_admin":false},"body":"What is temp?","created_at":"2015-08-07T13:23:39Z","updated_at":"2015-08-07T13:23:39Z","html_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets/pull/4#discussion_r36517702","pull_request_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4","_links":{"self":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/comments/36517702"},"html":{"href":"https://github.com/shotgunsoftware/tk-framework-qtwidgets/pull/4#discussion_r36517702"},"pull_request":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4"}}},"pull_request":{"url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4","id":30095461,"html_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets/pull/4","diff_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets/pull/4.diff","patch_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets/pull/4.patch","issue_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/4","number":4,"state":"open","locked":false,"title":"For #25503 - Changes and additions for Workfiles v2","user":{"login":"alandann","id":2451760,"avatar_url":"https://avatars.githubusercontent.com/u/2451760?v=3","gravatar_id":"","url":"https://api.github.com/users/alandann","html_url":"https://github.com/alandann","followers_url":"https://api.github.com/users/alandann/followers","following_url":"https://api.github.com/users/alandann/following{/other_user}","gists_url":"https://api.github.com/users/alandann/gists{/gist_id}","starred_url":"https://api.github.com/users/alandann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/alandann/subscriptions","organizations_url":"https://api.github.com/users/alandann/orgs","repos_url":"https://api.github.com/users/alandann/repos","events_url":"https://api.github.com/users/alandann/events{/privacy}","received_events_url":"https://api.github.com/users/alandann/received_events","type":"User","site_admin":false},"body":"This pull request tidies up some existing code (including renaming files for consistency, etc. - sorry!) and adds a bunch of shared widgets implemented as part of the Workfiles v2 work.  \r\n\r\nThe main changes are:\r\n\r\n- shotgun_view.WidgetDelegate now derives from a base implementation in views.WidgetDelegate so that the delegate can be used without the edit-on-selection mechanism.\r\n- WidgetDelegate interface has been extended with separate get_paint_widget() & create_editor_widget() methods to allow more fine grained control in derived classes.  The original create_widget() is still supported for backwards compatibility\r\n- Added a new ShotgunModelOverlayWidget that handles the connections to the shotgun model internally so that it's possible easily connect multiple overlay widgets to the same model.\r\n- Added a hierarchical filtering proxy model that handles searching/filtering across a hierarchical model in a generic way\r\n\r\nThe new widgets added are:\r\n\r\n__SpinnerWidget__\r\n![image](https://cloud.githubusercontent.com/assets/2451760/6396402/288c2e2a-bdd6-11e4-9e51-b054c90ff816.png)\r\nA simple animated widget used for displaying that something is busy.  This could be extended in the future to support an icon making it simple to replace the animation code in the overlay widget\r\n\r\n__SearchWidget__\r\n![image](https://cloud.githubusercontent.com/assets/2451760/6396558/068a1a66-bdd7-11e4-9da4-f2c301ff5000.png)\r\nA simple widget containing a text field, icon and clear button with signals that get emitted when the search text changes.\r\n\r\n__GroupedListView__\r\n![image](https://cloud.githubusercontent.com/assets/2451760/6396844/d43052d6-bdd8-11e4-832b-971400b1a62c.png)\r\nThis is a custom view that presents a hierarchical model as a grouped list of items - the root level are the groups, the second level are the items within groups and all lower levels are ignored.\r\n\r\n","created_at":"2015-02-26T16:59:57Z","updated_at":"2015-08-07T13:23:39Z","closed_at":null,"merged_at":null,"merge_commit_sha":"cef9526c5f24169899e3157e1d858ef92162e8cf","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4/commits","review_comments_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4/comments","review_comment_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/comments{/number}","comments_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/4/comments","statuses_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/statuses/d5b57274d688343691dffff4a526bcce669f65bc","head":{"label":"shotgunsoftware:25503_workfiles_v2","ref":"25503_workfiles_v2","sha":"d5b57274d688343691dffff4a526bcce669f65bc","user":{"login":"shotgunsoftware","id":317894,"avatar_url":"https://avatars.githubusercontent.com/u/317894?v=3","gravatar_id":"","url":"https://api.github.com/users/shotgunsoftware","html_url":"https://github.com/shotgunsoftware","followers_url":"https://api.github.com/users/shotgunsoftware/followers","following_url":"https://api.github.com/users/shotgunsoftware/following{/other_user}","gists_url":"https://api.github.com/users/shotgunsoftware/gists{/gist_id}","starred_url":"https://api.github.com/users/shotgunsoftware/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shotgunsoftware/subscriptions","organizations_url":"https://api.github.com/users/shotgunsoftware/orgs","repos_url":"https://api.github.com/users/shotgunsoftware/repos","events_url":"https://api.github.com/users/shotgunsoftware/events{/privacy}","received_events_url":"https://api.github.com/users/shotgunsoftware/received_events","type":"Organization","site_admin":false},"repo":{"id":19910716,"name":"tk-framework-qtwidgets","full_name":"shotgunsoftware/tk-framework-qtwidgets","owner":{"login":"shotgunsoftware","id":317894,"avatar_url":"https://avatars.githubusercontent.com/u/317894?v=3","gravatar_id":"","url":"https://api.github.com/users/shotgunsoftware","html_url":"https://github.com/shotgunsoftware","followers_url":"https://api.github.com/users/shotgunsoftware/followers","following_url":"https://api.github.com/users/shotgunsoftware/following{/other_user}","gists_url":"https://api.github.com/users/shotgunsoftware/gists{/gist_id}","starred_url":"https://api.github.com/users/shotgunsoftware/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shotgunsoftware/subscriptions","organizations_url":"https://api.github.com/users/shotgunsoftware/orgs","repos_url":"https://api.github.com/users/shotgunsoftware/repos","events_url":"https://api.github.com/users/shotgunsoftware/events{/privacy}","received_events_url":"https://api.github.com/users/shotgunsoftware/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets","description":"A collection of Toolkit related QT Widgets","fork":false,"url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets","forks_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/forks","keys_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/keys{/key_id}","collaborators_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/teams","hooks_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/hooks","issue_events_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/events{/number}","events_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/events","assignees_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/assignees{/user}","branches_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/branches{/branch}","tags_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/tags","blobs_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/refs{/sha}","trees_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/trees{/sha}","statuses_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/statuses/{sha}","languages_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/languages","stargazers_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/stargazers","contributors_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/contributors","subscribers_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/subscribers","subscription_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/subscription","commits_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/commits{/sha}","git_commits_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/commits{/sha}","comments_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/comments{/number}","issue_comment_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/comments{/number}","contents_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/contents/{+path}","compare_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/compare/{base}...{head}","merges_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/merges","archive_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/downloads","issues_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues{/number}","pulls_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls{/number}","milestones_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/milestones{/number}","notifications_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/labels{/name}","releases_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/releases{/id}","created_at":"2014-05-18T13:12:00Z","updated_at":"2015-08-04T01:40:55Z","pushed_at":"2015-07-31T14:15:20Z","git_url":"git://github.com/shotgunsoftware/tk-framework-qtwidgets.git","ssh_url":"git@github.com:shotgunsoftware/tk-framework-qtwidgets.git","clone_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets.git","svn_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets","homepage":"","size":4548,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"open_issues_count":1,"forks":3,"open_issues":1,"watchers":2,"default_branch":"master"}},"base":{"label":"shotgunsoftware:master","ref":"master","sha":"ab4f474538a3cde521ba254007f2b4e26338eaef","user":{"login":"shotgunsoftware","id":317894,"avatar_url":"https://avatars.githubusercontent.com/u/317894?v=3","gravatar_id":"","url":"https://api.github.com/users/shotgunsoftware","html_url":"https://github.com/shotgunsoftware","followers_url":"https://api.github.com/users/shotgunsoftware/followers","following_url":"https://api.github.com/users/shotgunsoftware/following{/other_user}","gists_url":"https://api.github.com/users/shotgunsoftware/gists{/gist_id}","starred_url":"https://api.github.com/users/shotgunsoftware/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shotgunsoftware/subscriptions","organizations_url":"https://api.github.com/users/shotgunsoftware/orgs","repos_url":"https://api.github.com/users/shotgunsoftware/repos","events_url":"https://api.github.com/users/shotgunsoftware/events{/privacy}","received_events_url":"https://api.github.com/users/shotgunsoftware/received_events","type":"Organization","site_admin":false},"repo":{"id":19910716,"name":"tk-framework-qtwidgets","full_name":"shotgunsoftware/tk-framework-qtwidgets","owner":{"login":"shotgunsoftware","id":317894,"avatar_url":"https://avatars.githubusercontent.com/u/317894?v=3","gravatar_id":"","url":"https://api.github.com/users/shotgunsoftware","html_url":"https://github.com/shotgunsoftware","followers_url":"https://api.github.com/users/shotgunsoftware/followers","following_url":"https://api.github.com/users/shotgunsoftware/following{/other_user}","gists_url":"https://api.github.com/users/shotgunsoftware/gists{/gist_id}","starred_url":"https://api.github.com/users/shotgunsoftware/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shotgunsoftware/subscriptions","organizations_url":"https://api.github.com/users/shotgunsoftware/orgs","repos_url":"https://api.github.com/users/shotgunsoftware/repos","events_url":"https://api.github.com/users/shotgunsoftware/events{/privacy}","received_events_url":"https://api.github.com/users/shotgunsoftware/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets","description":"A collection of Toolkit related QT Widgets","fork":false,"url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets","forks_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/forks","keys_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/keys{/key_id}","collaborators_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/teams","hooks_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/hooks","issue_events_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/events{/number}","events_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/events","assignees_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/assignees{/user}","branches_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/branches{/branch}","tags_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/tags","blobs_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/refs{/sha}","trees_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/trees{/sha}","statuses_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/statuses/{sha}","languages_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/languages","stargazers_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/stargazers","contributors_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/contributors","subscribers_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/subscribers","subscription_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/subscription","commits_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/commits{/sha}","git_commits_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/git/commits{/sha}","comments_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/comments{/number}","issue_comment_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/comments{/number}","contents_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/contents/{+path}","compare_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/compare/{base}...{head}","merges_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/merges","archive_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/downloads","issues_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues{/number}","pulls_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls{/number}","milestones_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/milestones{/number}","notifications_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/labels{/name}","releases_url":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/releases{/id}","created_at":"2014-05-18T13:12:00Z","updated_at":"2015-08-04T01:40:55Z","pushed_at":"2015-07-31T14:15:20Z","git_url":"git://github.com/shotgunsoftware/tk-framework-qtwidgets.git","ssh_url":"git@github.com:shotgunsoftware/tk-framework-qtwidgets.git","clone_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets.git","svn_url":"https://github.com/shotgunsoftware/tk-framework-qtwidgets","homepage":"","size":4548,"stargazers_count":2,"watchers_count":2,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"open_issues_count":1,"forks":3,"open_issues":1,"watchers":2,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4"},"html":{"href":"https://github.com/shotgunsoftware/tk-framework-qtwidgets/pull/4"},"issue":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/4"},"comments":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/issues/4/comments"},"review_comments":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4/comments"},"review_comment":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/pulls/4/commits"},"statuses":{"href":"https://api.github.com/repos/shotgunsoftware/tk-framework-qtwidgets/statuses/d5b57274d688343691dffff4a526bcce669f65bc"}}}}