예제 #1
0
  def __init__(self,parent,name,name1='',udi_root=None,caption=None,prec=(None,'g'),maxwidth=None):
    self._tw = DataDraggableTreeWidget(ClickableTreeWidget)(parent);
    self._tw.setHeaderLabels([name1,'',name]);
    self._tw.setRootIsDecorated(True);
    self._tw.setSortingEnabled(False);
    self._tw.header().setResizeMode(0,QHeaderView.ResizeToContents);
    self._tw.header().setResizeMode(1,QHeaderView.ResizeToContents);
    self._tw.header().setResizeMode(2,QHeaderView.ResizeToContents);
    self._tw.header().setStretchLastSection(False);
    self._tw.header().setResizeMode(QHeaderView.ResizeToContents);

    self._tw.header().hide();
    setattr(self._tw,'_maxwidth',maxwidth or HierBrowser.MaxWidth);
    # self._tw.header().setResizeMode(QHeaderView.Fixed);
#    for col in (0,1,2):
#      self._tw.setColumnWidthMode(col,QListView.Maximum);
#    self._tw.setFocus();
    QObject.connect(self._tw,SIGNAL('itemExpanded(QTreeWidgetItem*)'),
                     self._expand_item_content);
    QObject.connect(self._tw,PYSIGNAL('mouseButtonClicked()'),self._process_item_click);
    QObject.connect(self._tw,PYSIGNAL('itemContextMenuRequested()'),self._show_context_menu);
#    self._tw.connect(self._tw,SIGNAL('doubleClicked(QListViewItem*)'),
#                     self.display_item);
    # connect the get_drag_item method for drag-and-drop
    self._tw.get_drag_item = self.get_drag_item;
    self._tw.get_drag_item_type = self.get_drag_item_type;
    # enable UDIs, if udi root is not none
    self.set_udi_root(udi_root);
    # initialize precision
    self._tw._prec = prec;
    # for debugging purposes
    QObject.connect(self._tw,SIGNAL("itemClicked(QTreeWidgetItem*)"),self._print_item);
예제 #2
0
class HierBrowser(object):
    # seqs/dicts with <= items than this are treated as "short"
    ShortSeq = 5
    # maximum number of sequence items to show in expanded view
    MaxExpSeq = 1000
    # max number of dictionary items to show in expanded view
    MaxExpDict = 5000
    # maximum width of value field, in characters
    MaxWidth = 400

    class Item(QTreeWidgetItem):
        def __init__(self,
                     parent,
                     key,
                     value,
                     udi_key=None,
                     udi=None,
                     parent_udi=None,
                     strfunc=None,
                     prec=(None, 'g'),
                     name=None,
                     caption=None,
                     desc=''):
            (key, value) = (str(key), str(value))
            # insert item at end of parent's content list (if any)
            if parent:
                parent_content = getattr(parent, '_content_list', None)
                if parent_content:
                    QTreeWidgetItem.__init__(self, parent, parent_content[-1])
                    parent_content.append(self)
                else:
                    QTreeWidgetItem.__init__(self, parent)
                    if parent:
                        parent._content_list = [self]
            else:
                QTreeWidgetItem.__init__(self)
            # get maxwidth
            maxwidth = getattr(self.treeWidget(), '_maxwidth',
                               HierBrowser.MaxWidth)
            # set flags
            self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
            # set text
            self.setText(0, str(key))
            self.setText(1, '')
            strvalue = str(value)
            self.setToolTip(2, "<P>" + strvalue + "</P>")
            if len(strvalue) > maxwidth:
                strvalue = strvalue[:maxwidth - 2] + "..."
            self.setText(2, strvalue)
            # set viewable and content attributes
            self._viewable = False
            self._content = None
            # set the refresh function and precision, if specified
            self._strfunc = strfunc
            self._prec = prec
            self._prec_menu = None
            # is udi directly specified?
            if udi:
                self._udi = udi
                self._udi_key = udi_key or udi
            else:
                # else generate udi key if none is specified
                if udi_key is id:
                    udi_key = "%-X" % (id(self), )
                elif udi_key is None:
                    udi_key = key
                self._udi_key = udi_key
                # setup udi of self, if parent self has a udi of its own
                # add ourselves to content map and propagate the map
                parent_udi = parent_udi or (parent and parent._udi)
                if parent_udi:
                    self._udi = '/'.join((parent_udi, udi_key))
                else:
                    self._udi = None
            # add to content map, if we have a UDI
            if self._udi and parent:
                self.treeWidget()._content_map[self._udi] = self
            # set name and/or description
            self._name = name
            self._desc = desc
            self._caption = caption
            # other state
            self._curries = []

        def set_udi(self, udi):
            self.treeWidget()._content_map[udi] = self
            self._udi = udi

        # caches content in an item: marks as expandable, ensures content is a dict
        # if viewable is None, decides if content is viewable based on its type
        # else, must be True or False to specify viewability explicitly
        def cache_content(self,
                          content,
                          viewable=None,
                          viewopts={},
                          make_data=None):
            # if already have content, remove it (and remove all children)
            try:
                delattr(self, '_content')
            except:
                pass
            try:
                delattr(self, '_content_list')
            except:
                pass
            self.takeChildren()
            ### only available in Qt4.4
            ## self.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator);
            # make dummy child item to ensure that child indicatro is shown
            dum = QTreeWidgetItem(self)
            # convert all content to dict
            if isinstance(content, (dict, list, tuple, array_class)):
                self._content = content
            elif isinstance(content, message):
                self._content = {}
                for k in filter(lambda x: not x.startswith('_'), dir(content)):
                    attr = getattr(content, k)
                    if not callable(attr):
                        self._content[k] = attr
            else:
                self._content = {
                    "": content
                }
            # set the viewable property
            self.set_viewable(content, viewable, viewopts)
            # set the make_data property
            self._make_data = make_data

        def set_viewable(self, content, viewable=None, viewopts={}):
            # set the viewable property
            if not self._udi:
                viewable = False
            elif viewable is None:
                viewable = Grid.Services.isViewable(content)
            self._viewable = viewable
            if viewable:
                self._viewopts = viewopts
                self.setIcon(1, pixmaps.viewmag_plus.icon())
                self.setFlags(self.flags() | Qt.ItemIsDragEnabled)

        # helper static method to expand content into Items record
        # note that we make this a static method because 'item' may in fact be
        # a parent QListView, the expansion interface is the same
        def expand_content(item, content):
            # content_list attribute is initialized upon expansion
            if hasattr(item, '_content_list'):
                return
            # remove children of item, if any
            if hasattr(item, 'takeChildren'):
                item.takeChildren()
            item._content_list = []
            # Setup content_iter as an iterator that returns (label,value)
            # pairs, depending on content type.
            # Apply limits here
            if isinstance(content, dict):
                n = len(content) - HierBrowser.MaxExpDict
                if n > 0:
                    keys = content.keys()[:HierBrowser.MaxExpDict]
                    content_iter = map(lambda k: (k, content[k]), keys)
                    content_iter.append(
                        ('...', '...(%d items skipped)...' % n))
                else:
                    content_iter = content.iteritems()
            elif isinstance(content, (list, tuple, array_class)):
                n = len(content) - HierBrowser.MaxExpSeq
                if n > 0:
                    content_iter = list(
                        enumerate(content[:HierBrowser.MaxExpSeq - 2]))
                    content_iter.append(
                        ('...', '...(%d items skipped)...' % (n + 1)))
                    content_iter.append((len(content) - 1, content[-1]))
                else:
                    content_iter = enumerate(content)
            else:
                content_iter = (("", content), )
            for (key, value) in content_iter:
                # simplest case: do we have an inlined to-string converter?
                # then the value is represented by a single item
                # use curry() to create a refresh-function
                (itemstr, inlined) = dmirepr.inline_str(value)
                if itemstr is not None:
                    i0 = HierBrowser.Item(None,
                                          key,
                                          itemstr,
                                          prec=item._prec,
                                          parent_udi=item._udi,
                                          strfunc=curry(
                                              dmirepr.inline_str, value))
                    i0._content = value
                    i0.set_viewable(value)
                    item._content_list.append(i0)
                    continue
                # else get string representation, insert item with it
                (itemstr, inlined) = dmirepr.expanded_repr_str(value, False)
                i0 = HierBrowser.Item(None,
                                      str(key),
                                      itemstr,
                                      prec=item._prec,
                                      parent_udi=item._udi,
                                      strfunc=curry(dmirepr.expanded_repr_str,
                                                    value, False))
                item._content_list.append(i0)
                # cache value for expansion, if not inlined
                if isinstance(value, (list, tuple)):
                    if len(value) > 1 or not inlined:
                        i0.cache_content(value)
                elif isinstance(value, array_class):
                    if value.size > 1 or not inlined:
                        i0.cache_content(value)
                # dicts and messages always cached for expansion
                elif isinstance(value, (dict, message)):
                    i0.cache_content(value)
            if hasattr(item, 'addChildren'):
                item.addChildren(item._content_list)
            else:
                item.addTopLevelItems(item._content_list)

        expand_content = staticmethod(expand_content)

        # expands item content into subitems (wrapper around expand_content)
        def expand_self(self):
            self.expand_content(self, self._content)

        def make_data_item(self, viewer=None, viewopts={}):
            make_data = getattr(self, '_make_data', None)
            if callable(make_data):
                return make_data(viewer=None, viewopts={})
            # return item only if viewable, has udi and contents
            if self._content is not None and self._udi and self._viewable:
                vo = getattr(self, '_viewopts', {})
                vo.update(viewopts)
                name = self._name
                desc = self._desc
                if not (name or desc):
                    desc = self._udi
                # a refresh function may be defined in the list view
                refresh = getattr(self.treeWidget(), '_refresh_func', None)
                # make item and return
                if not self._name and not self._caption:
                    (name, caption) = meqds.make_udi_caption(self._udi)
                else:
                    (name, caption) = (self._name
                                       or self._caption, self._caption
                                       or self._name)
                return Grid.DataItem(self._udi,
                                     name=name,
                                     caption=caption,
                                     data=self._content,
                                     viewer=viewer,
                                     viewopts=vo,
                                     refresh=refresh)
            return None

        def get_precision(self):
            return self._prec

        # changes display precision of item
        def set_precision(self, prec, set_menu=True, recursive=True):
            self._prec = prec
            if callable(self._strfunc):
                (txt, inlined) = self._strfunc(prec=self._prec)
                self.setText(2, txt)
            # set menu checkmark, if any
            if set_menu and self._prec_menu:
                self._prec_menu.set(prec)
            # recursively apply to children
            if recursive:
                for i in range(self.childCount()):
                    self.child(i).set_precision(prec)

        def _set_prec_frommenu(self, prec, format):
            self.set_precision((prec, format), set_menu=False)

        # curries and adds to local list
        # This is useful for creating on-the-fly callbacks
        # (since most Qt callbacks are held via weakref, they're deleted immediately
        # unless strongly referenced elsewhere)
        def curry(self, *args, **kw):
            c = curry(*args, **kw)
            self._curries.append(c)
            return c

        def xcurry(self, *args, **kw):
            c = xcurry(*args, **kw)
            self._curries.append(c)
            return c

        # dum argument needed to use as callback from popup menu
        def copy_to_clipboard(self, dum=None):
            # text = str(self.text(0))+": "+str(self.text(2));
            text = str(self.text(2))
            QApplication.clipboard().setText(text)
            QApplication.clipboard().setText(text, QClipboard.Selection)

        def get_context_menu(self):
            try:
                menu = self._context_menu
            except AttributeError:
                # create menu on the fly when first called for this item
                viewer_list = self._content is not None and self._viewable and \
                           Grid.Services.getViewerList(self._content,self._udi)
                # create menu
                menu = self._context_menu = QMenu(self.treeWidget())
                menu._callbacks = []
                menu.addAction(self._desc or self._name
                               or str(self.text(0))).setSeparator(True)
                # insert Copy item
                copy_qa = menu.addAction(pixmaps.editcopy.icon(),
                                         '&Copy to clipboard',
                                         self.copy_to_clipboard,
                                         Qt.CTRL + Qt.Key_Insert)
                # create "Precision" submenu
                if self._strfunc:
                    self._prec_menu = PrecisionPopupMenu(menu, prec=self._prec)
                    qa = menu.addMenu(self._prec_menu)
                    qa.setText('Number format')
                    qa.setIcon(pixmaps.precplus.icon())
                    QWidget.connect(self._prec_menu,
                                    PYSIGNAL("setPrecision()"),
                                    self._set_prec_frommenu)
                # create "display with" entries
                if viewer_list:
                    # create display submenus
                    menu1 = self._display_menu1 = menu.addMenu(
                        pixmaps.viewmag.icon(), "Display with")
                    menu2 = self._display_menu2 = menu.addMenu(
                        pixmaps.viewmag_plus.icon(), "New display with")
                    for v in viewer_list:
                        # create entry for viewer
                        name = getattr(v, 'viewer_name', v.__name__)
                        try:
                            icon = v.icon()
                        except AttributeError:
                            icon = QIcon()
                        # add entry to both menus ("Display with" and "New display with")
                        menu1.addAction(icon,name, \
                          self.xcurry(self.emit_display_signal,viewer=v,_argslice=slice(0)))
                        menu2.addAction(icon,name, \
                          self.xcurry(self.emit_display_signal,viewer=v,newcell=True,_argslice=slice(0)))
            # set the precision submenu to the right setting
            return menu

        # if item is displayable, creates a dataitem from it and
        # emits a displayDataItem(dataitem,cellspec) signal
        def emit_display_signal(self, viewer=None, **kwargs):
            _dprint(2, "emitting displayDataItem() signal")
            dataitem = self.make_data_item(viewer=viewer)
            if dataitem:
                self.treeWidget().emit(SIGNAL("displayDataItem"), dataitem,
                                       kwargs)

    # init for HierBrowser
    def __init__(self,
                 parent,
                 name,
                 name1='',
                 udi_root=None,
                 caption=None,
                 prec=(None, 'g'),
                 maxwidth=None):
        self._tw = DataDraggableTreeWidget(ClickableTreeWidget)(parent)
        self._tw.setHeaderLabels([name1, '', name])
        self._tw.setRootIsDecorated(True)
        self._tw.setSortingEnabled(False)
        self._tw.header().setResizeMode(0, QHeaderView.ResizeToContents)
        self._tw.header().setResizeMode(1, QHeaderView.ResizeToContents)
        self._tw.header().setResizeMode(2, QHeaderView.ResizeToContents)
        self._tw.header().setStretchLastSection(False)
        self._tw.header().setResizeMode(QHeaderView.ResizeToContents)

        self._tw.header().hide()
        setattr(self._tw, '_maxwidth', maxwidth or HierBrowser.MaxWidth)
        # self._tw.header().setResizeMode(QHeaderView.Fixed);
        #    for col in (0,1,2):
        #      self._tw.setColumnWidthMode(col,QListView.Maximum);
        #    self._tw.setFocus();
        QObject.connect(self._tw, SIGNAL('itemExpanded(QTreeWidgetItem*)'),
                        self._expand_item_content)
        QObject.connect(self._tw, PYSIGNAL('mouseButtonClicked()'),
                        self._process_item_click)
        QObject.connect(self._tw, PYSIGNAL('itemContextMenuRequested()'),
                        self._show_context_menu)
        #    self._tw.connect(self._tw,SIGNAL('doubleClicked(QListViewItem*)'),
        #                     self.display_item);
        # connect the get_drag_item method for drag-and-drop
        self._tw.get_drag_item = self.get_drag_item
        self._tw.get_drag_item_type = self.get_drag_item_type
        # enable UDIs, if udi root is not none
        self.set_udi_root(udi_root)
        # initialize precision
        self._tw._prec = prec
        # for debugging purposes
        QObject.connect(self._tw, SIGNAL("itemClicked(QTreeWidgetItem*)"),
                        self._print_item)

    def get_precision(self):
        return self._tw._prec

    # changes display precision of view
    def set_precision(self, prec, recursive=True):
        self._tw._prec = prec
        if recursive:
            for i in range(self._tw.topLevelItemCount()):
                self._tw.topLevelItem(i).set_precision(prec)

    def set_refresh_func(self, refresh):
        self._tw._refresh_func = refresh

    def set_udi_root(self, udi_root):
        self._udi_root = udi_root
        if udi_root is not None:
            if not udi_root.startswith('/'):
                udi_root = "/" + udi_root
            self._tw._udi = udi_root
            # map of UDIs to items
            self._tw._content_map = weakref.WeakValueDictionary()
        else:
            self._tw._udi = None

    def _print_item(self, item, *dum):
        if item is not None:
            _dprint(4, 'item:', item.text(0), item.text(2))
            for attr in ('_udi', '_udi_key', '_viewable', '_name', '_desc'):
                if hasattr(item, attr):
                    _dprint(4, ' ', attr + ':', getattr(item, attr))
            try:
                lencont = len(item.treeWidget()._content_map)
                _dprint(4, '  _content_map: ', lencont, ' items')
            except AttributeError:
                pass

    def get_drag_item(self, key):
        item = self._tw._content_map.get(key, None)
        return item and item.make_data_item()

    def get_drag_item_type(self, key):
        return key in self._tw._content_map and Timba.Grid.DataItem

    def treeWidget(self):
        return self._tw

    def wtop(self):
        return self._tw

    def clear(self):
        self._tw.clear()
        for attr in ('_content', ):
            try:
                delattr(self._tw, attr)
            except:
                pass
        self.wtop().emit(SIGNAL("cleared"))

    # limits browser to last 'limit' items
    def apply_limit(self, limit):
        num_items = self._tw.topLevelItemCount()
        if num_items > limit:
            # remove N items from the beginning of the QTreeWidget
            items = [
                self._tw.takeTopLevelItem(0) for i in range(num_items - limit)
            ]
            # add them to a dummy widget, which will then be destroyed -- this ensures the items get destoryed as well
            dum = QTreeWidget()
            dum.insertTopLevelItems(0, items)
            dum = None

    # called when an item is expanded
    def _expand_item_content(self, item):
        item.expand_self()

    # slot: called when one of the items is clicked
    def _process_item_click(self, button, item, pos, col):
        if col == 1:
            if button == Qt.LeftButton:
                item.emit_display_signal()
            elif button == Qt.MidButton:
                item.emit_display_signal(newcell=True)

    # slot: called to show a context menu for an item
    def _show_context_menu(self, item, pos, col):
        menu = item and item.get_context_menu()
        if menu is not None:
            menu.exec_(self._tw.mapToGlobal(pos))

    def get_open_items(self):
        """gets tree of currently open and selected items. Returns tuple of
    (dict,<str|None>), describing the state of top-level items. The dict keys
    are udi_keys of expanded items; the dict values are similar tuples
    describing the state of each sub-level. The second element of the tuple is
    the key of the currently selected item, or None if no items are selected at
    this level; normally, at most one entry in the entire tree has a selected
    item. A None value in place of a tuple indicates no open and no selected
    items."""

        # recursive helper function implementing tree traversal
        def _get_open_items_impl(parent, current):
            openitems = {}
            current_key = None
            for i in range(parent.childCount()):
                item = parent.child(i)
                if item is current:
                    current_key = item._udi_key
                if item.isExpanded():
                    openitems[item._udi_key] = _get_open_items_impl(
                        item, current)
            if openitems or current_key:
                return (openitems, current_key)
            return None

        return _get_open_items_impl(self._tw.invisibleRootItem(),
                                    self._tw.currentItem())

    def set_open_items(self, openspec):
        """sets currently open and selected items according to tree returned
    by a previous get_open_items() call."""

        # recursive helper function implementing tree traversal
        def _set_open_items_impl(parent, openspec):
            select = None
            if openspec is None:
                return None
            (openitems, current_key) = openspec
            for i in range(parent.childCount()):
                item = parent.child(i)
                if item._udi_key == current_key:
                    select = item
                # if item is open, expand it and go in recursively
                if item._udi_key in openitems:
                    item.setExpanded(True)
                    select = select or _set_open_items_impl(
                        item, openitems[item._udi_key])
            return select

        # call recursive helper on listview, select returned item
        select = _set_open_items_impl(self._tw.invisibleRootItem(), openspec)
        if select:
            self._tw.setCurrentItem(select)
            self._tw.scrollToItem(select)

    def set_content(self, content):
        # expand first level of record
        self.clear()
        # call Item.expand_content() with our listview as the first argument,
        # since that method is universal for both Items an ListViews
        HierBrowser.Item.expand_content(self._tw, content)

    def change_item_content(self, item, content, keepopen=True, **kwargs):
        """changes content of an item. If keepopen=True and item was open,
    attempts to preserve open structure and selected items"""
        openitems = None
        if item.isExpanded():
            openitems = (keepopen and self.get_open_items()) or None
            item.setExpanded(False)
        item.cache_content(content, **kwargs)
        if openitems:
            self.set_open_items(openitems)