예제 #1
0
def details_context_menu_event(view, ev, book_info, add_popup_action=False):
    url = view.anchorAt(ev.pos())
    menu = QMenu(view)
    menu.addAction(QIcon(I('edit-copy.png')), _('Copy all book details'),
                   partial(copy_all, view))
    search_internet_added = False
    if url and url.startswith('action:'):
        data = json_loads(from_hex_bytes(url.split(':', 1)[1]))
        search_internet_added = add_item_specific_entries(
            menu, data, book_info)
    elif url and not url.startswith('#'):
        ac = book_info.copy_link_action
        ac.current_url = url
        ac.setText(_('Copy link location'))
        menu.addAction(ac)
    if not search_internet_added and hasattr(book_info, 'search_internet'):
        menu.addSeparator()
        menu.si = create_search_internet_menu(book_info.search_internet)
        menu.addMenu(menu.si)
    for ac in tuple(menu.actions()):
        if not ac.isEnabled():
            menu.removeAction(ac)
    if add_popup_action:
        menu.addSeparator()
        ac = menu.addAction(_('Open the Book details window'))
        ac.triggered.connect(book_info.show_book_info)
    if len(menu.actions()) > 0:
        menu.exec_(ev.globalPos())
예제 #2
0
파일: main.py 프로젝트: j-howell/calibre
    def show_context_menu(self, point):
        item = self.currentItem()

        def key(k):
            sc = unicode_type(QKeySequence(k | Qt.CTRL).toString(QKeySequence.NativeText))
            return ' [%s]'%sc

        if item is not None:
            m = QMenu()
            m.addAction(QIcon(I('edit_input.png')), _('Change the location this entry points to'), self.edit_item)
            m.addAction(QIcon(I('modified.png')), _('Bulk rename all selected items'), self.bulk_rename)
            m.addAction(QIcon(I('trash.png')), _('Remove all selected items'), self.del_items)
            m.addSeparator()
            ci = unicode_type(item.data(0, Qt.DisplayRole) or '')
            p = item.parent() or self.invisibleRootItem()
            idx = p.indexOfChild(item)
            if idx > 0:
                m.addAction(QIcon(I('arrow-up.png')), (_('Move "%s" up')%ci)+key(Qt.Key_Up), self.move_up)
            if idx + 1 < p.childCount():
                m.addAction(QIcon(I('arrow-down.png')), (_('Move "%s" down')%ci)+key(Qt.Key_Down), self.move_down)
            if item.parent() is not None:
                m.addAction(QIcon(I('back.png')), (_('Unindent "%s"')%ci)+key(Qt.Key_Left), self.move_left)
            if idx > 0:
                m.addAction(QIcon(I('forward.png')), (_('Indent "%s"')%ci)+key(Qt.Key_Right), self.move_right)

            m.addSeparator()
            case_menu = QMenu(_('Change case'))
            case_menu.addAction(_('Upper case'), self.upper_case)
            case_menu.addAction(_('Lower case'), self.lower_case)
            case_menu.addAction(_('Swap case'), self.swap_case)
            case_menu.addAction(_('Title case'), self.title_case)
            case_menu.addAction(_('Capitalize'), self.capitalize)
            m.addMenu(case_menu)

            m.exec_(QCursor.pos())
예제 #3
0
    def contextMenuEvent(self, ev):
        from calibre.gui2.open_with import populate_menu, edit_programs

        cm = QMenu(self)
        paste = cm.addAction(_("Paste Cover"))
        copy = cm.addAction(_("Copy Cover"))
        remove = cm.addAction(_("Remove Cover"))
        gc = cm.addAction(_("Generate Cover from metadata"))
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)

        m = QMenu(_("Open cover with..."))
        populate_menu(m, self.open_with, "cover_image")
        if len(m.actions()) == 0:
            cm.addAction(_("Open cover with..."), self.choose_open_with)
        else:
            m.addSeparator()
            m.addAction(_("Add another application to open cover..."), self.choose_open_with)
            m.addAction(_("Edit Open With applications..."), partial(edit_programs, "cover_image", self))
            cm.addMenu(m)
        cm.exec_(ev.globalPos())
예제 #4
0
    def contextMenuEvent(self, ev):
        from calibre.gui2.open_with import populate_menu, edit_programs
        cm = QMenu(self)
        paste = cm.addAction(_('Paste cover'))
        copy = cm.addAction(_('Copy cover'))
        remove = cm.addAction(_('Remove cover'))
        gc = cm.addAction(_('Generate cover from metadata'))
        cm.addSeparator()
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)

        m = QMenu(_('Open cover with...'))
        populate_menu(m, self.open_with, 'cover_image')
        if len(m.actions()) == 0:
            cm.addAction(_('Open cover with...'), self.choose_open_with)
        else:
            m.addSeparator()
            m.addAction(_('Add another application to open cover...'), self.choose_open_with)
            m.addAction(_('Edit Open with applications...'), partial(edit_programs, 'cover_image', self))
            cm.ocw = m
            cm.addMenu(m)
        cm.si = m = create_search_internet_menu(self.search_internet.emit)
        cm.addMenu(m)
        cm.exec_(ev.globalPos())
예제 #5
0
    def show_context_menu(self, point):
        item = self.currentItem()

        def key(k):
            sc = unicode_type(QKeySequence(k | Qt.CTRL).toString(QKeySequence.NativeText))
            return ' [%s]'%sc

        if item is not None:
            m = QMenu(self)
            m.addAction(QIcon(I('edit_input.png')), _('Change the location this entry points to'), self.edit_item)
            m.addAction(QIcon(I('modified.png')), _('Bulk rename all selected items'), self.bulk_rename)
            m.addAction(QIcon(I('trash.png')), _('Remove all selected items'), self.del_items)
            m.addSeparator()
            ci = unicode_type(item.data(0, Qt.DisplayRole) or '')
            p = item.parent() or self.invisibleRootItem()
            idx = p.indexOfChild(item)
            if idx > 0:
                m.addAction(QIcon(I('arrow-up.png')), (_('Move "%s" up')%ci)+key(Qt.Key_Up), self.move_up)
            if idx + 1 < p.childCount():
                m.addAction(QIcon(I('arrow-down.png')), (_('Move "%s" down')%ci)+key(Qt.Key_Down), self.move_down)
            if item.parent() is not None:
                m.addAction(QIcon(I('back.png')), (_('Unindent "%s"')%ci)+key(Qt.Key_Left), self.move_left)
            if idx > 0:
                m.addAction(QIcon(I('forward.png')), (_('Indent "%s"')%ci)+key(Qt.Key_Right), self.move_right)

            m.addSeparator()
            case_menu = QMenu(_('Change case'), m)
            case_menu.addAction(_('Upper case'), self.upper_case)
            case_menu.addAction(_('Lower case'), self.lower_case)
            case_menu.addAction(_('Swap case'), self.swap_case)
            case_menu.addAction(_('Title case'), self.title_case)
            case_menu.addAction(_('Capitalize'), self.capitalize)
            m.addMenu(case_menu)

            m.exec_(QCursor.pos())
예제 #6
0
    def initUI(self):
        # 右键菜单
        trayIconMenu = QMenu()
        bindAction = trayIconMenu.addAction('绑定帐号')
        subMenu1 = trayIconMenu.addMenu('登录')
        self.loginComAction = subMenu1.addAction('电脑版登录')
        loginMobAction = subMenu1.addAction('手机版登录')
        subMenu2 = trayIconMenu.addMenu('退出登录')
        logoutComAction = subMenu2.addAction('退出电脑登录')
        logoutMobAction = subMenu2.addAction('退出手机登录')
        logoutAllAction = subMenu2.addAction('退出全部登录')
        infoSearchAction = trayIconMenu.addAction('帐户信息查询')
        helpAction = trayIconMenu.addAction('帮助')
        aboutAction = trayIconMenu.addAction('关于')
        quitAction = trayIconMenu.addAction('退出')
        # 绑定Action
        bindAction.triggered.connect(self.bindUser)
        self.loginComAction.triggered.connect(lambda: self.login(1))
        loginMobAction.triggered.connect(lambda: self.login(2))
        logoutComAction.triggered.connect(lambda: self.logout(1))
        logoutMobAction.triggered.connect(lambda: self.logout(2))
        logoutAllAction.triggered.connect(lambda: self.logout(3))
        helpAction.triggered.connect(self.help)
        aboutAction.triggered.connect(self.about)
        quitAction.triggered.connect(qApp.quit)

        icon = QIcon('icon.png')
        self.setIcon(icon)
        self.setContextMenu(trayIconMenu)
        self.setToolTip('IPGW助手')
        self.show()
예제 #7
0
def details_context_menu_event(view,
                               ev,
                               book_info,
                               add_popup_action=False,
                               edit_metadata=None):
    url = view.anchorAt(ev.pos())
    menu = QMenu(view)
    copy_menu = menu.addMenu(QIcon(I('edit-copy.png')), _('Copy'))
    copy_menu.addAction(QIcon(I('edit-copy.png')), _('All book details'),
                        partial(copy_all, view))
    if view.textCursor().hasSelection():
        copy_menu.addAction(QIcon(I('edit-copy.png')), _('Selected text'),
                            view.copy)
    copy_menu.addSeparator()
    copy_links_added = False
    search_internet_added = False
    search_menu = QMenu(_('Search'), menu)
    search_menu.setIcon(QIcon(I('search.png')))
    if url and url.startswith('action:'):
        data = json_loads(from_hex_bytes(url.split(':', 1)[1]))
        search_internet_added = add_item_specific_entries(
            menu, data, book_info, copy_menu, search_menu)
        create_copy_links(copy_menu, data)
        copy_links_added = True
    elif url and not url.startswith('#'):
        ac = book_info.copy_link_action
        ac.current_url = url
        ac.setText(_('Copy link location'))
        menu.addAction(ac)
    if not copy_links_added:
        create_copy_links(copy_menu)

    if not search_internet_added and hasattr(book_info, 'search_internet'):
        sim = create_search_internet_menu(book_info.search_internet)
        if search_menu.isEmpty():
            search_menu = sim
        else:
            search_menu.addSeparator()
            for ac in sim.actions():
                search_menu.addAction(ac)
                ac.setText(_('Search {0} for this book').format(ac.text()))
    if not search_menu.isEmpty():
        menu.addMenu(search_menu)
    for ac in tuple(menu.actions()):
        if not ac.isEnabled():
            menu.removeAction(ac)
    menu.addSeparator()
    if add_popup_action:
        ac = menu.addAction(_('Open the Book details window'))
        ac.triggered.connect(book_info.show_book_info)
    else:
        from calibre.gui2.ui import get_gui
        ema = get_gui().iactions['Edit Metadata'].menuless_qaction
        menu.addAction(
            _('Open the Edit metadata window') + '\t' +
            ema.shortcut().toString(QKeySequence.SequenceFormat.NativeText),
            edit_metadata)
    if len(menu.actions()) > 0:
        menu.exec_(ev.globalPos())
예제 #8
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort tabs alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.new_api.pref('virt_libs_hidden')
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide Virtual library tabs'), self.disable_bar)
     if gprefs['vl_tabs_closable']:
         m.addAction(_('Lock Virtual library tabs'), self.lock_tab)
     else:
         m.addAction(_('Unlock Virtual library tabs'), self.unlock_tab)
     i = self.tabAt(ev.pos())
     if i > -1:
         vl = unicode_type(self.tabData(i) or '')
         if vl:
             m.addSeparator()
             m.addAction(
                 _('Edit "%s"') % vl,
                 partial(self.gui.do_create_edit, name=vl))
             m.addAction(
                 _('Delete "%s"') % vl,
                 partial(self.gui.remove_vl_triggered, name=vl))
     m.exec_(ev.globalPos())
예제 #9
0
파일: init.py 프로젝트: pombreda/calibre-1
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.prefs['virt_libs_hidden']
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide virtual library tabs'), self.disable_bar)
     m.exec_(ev.globalPos())
예제 #10
0
 def contextMenuEvent(self, ev):
     cm = QMenu(self)
     paste = cm.addAction(_('Paste cover'))
     copy = cm.addAction(_('Copy cover'))
     save = cm.addAction(_('Save cover to disk'))
     remove = cm.addAction(_('Remove cover'))
     gc = cm.addAction(_('Generate cover from metadata'))
     cm.addSeparator()
     if not QApplication.instance().clipboard().mimeData().hasImage():
         paste.setEnabled(False)
     copy.triggered.connect(self.copy_to_clipboard)
     paste.triggered.connect(self.paste_from_clipboard)
     remove.triggered.connect(self.remove_cover)
     gc.triggered.connect(self.generate_cover)
     save.triggered.connect(self.save_cover)
     create_open_cover_with_menu(self, cm)
     cm.si = m = create_search_internet_menu(self.search_internet.emit)
     cm.addMenu(m)
     cm.exec_(ev.globalPos())
예제 #11
0
파일: init.py 프로젝트: amorphous1/calibre
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.prefs['virt_libs_hidden']
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide virtual library tabs'), self.disable_bar)
     m.exec_(ev.globalPos())
예제 #12
0
    def contextMenuEvent(self, e):
        if self.selectionModel().selection().indexes():
            for i in self.selectionModel().selection().indexes():
                row, column = i.row(), i.column()
            player_name = self.window.listWidget.selectedItems()[0].text()
            menu = QMenu(self)

            op_action = QAction("Op", self.window)
            op_action.setIcon(self.get_icon('head.png'))
            menu.addAction(op_action)

            deop_action = QAction('De-Op', self)
            deop_action.setIcon(self.get_icon('head.png'))
            menu.addAction(deop_action)

            kick_action = QAction("Kick", self)
            kick_action.setIcon(self.get_icon('head.png'))
            menu.addAction(kick_action)

            ban_action = QAction("Ban", self)
            ban_action.setIcon(self.get_icon('head.png'))
            menu.addAction(ban_action)

            gamemode = menu.addMenu('Gamemode')
            gamemode.setIcon(self.get_icon('head.png'))
            gamemode.addAction('Creative')
            gamemode.addAction('Survival')
            gamemode.addAction('Adventure')

            action = menu.exec_(self.mapToGlobal(e.pos()))
            if action is None:
                return
            action = action.text()
            if "Kick" in action or "Ban" in action:
                for index, player in enumerate(self.players):
                    if player['name'] == player_name:
                        self.remove_player_by_index(index)
                if "Kick" in action:
                    self.window.execute_input("kick %s" % player_name)
                else:
                    self.window.execute_input("ban %s" % player_name)

                self.window.update_players()
            elif "Op" == action:
                self.window.execute_input("op %s" % player_name)
                ##TODO Update players op dict value maybe?
            elif "De-Op" in action:
                self.window.execute_input("deop %s" % player_name)
            elif "Survival" in action:
                self.window.execute_input("gamemode survival %s" % player_name)
            elif "Creative" in action:
                self.window.execute_input("gamemode creative %s" % player_name)
            elif "Adventure" in action:
                self.window.execute_input("gamemode adventure %s" %
                                          player_name)
    def make_context_menu(self,
                          undo_stack,
                          project_tree_controller,
                          parent=None):
        """Creates a context menu for this node.
    undo_stack -- QUndoStack that should receive undoable editing commands generated by the menu.
    project_tree_controller -- ProjectTreeController in charge of high-level changes to the project tree.
    parent -- Parent QObject for the context menu.
    """
        menu = QMenu(parent)

        # Add menu items for creating child nodes, if the node is allowed to have children.
        if self.can_have_children:

            def add_dir_func():
                new_node = DirProjectTreeNode(
                    self.suggest_child_name(_DIR_NODE_NAME_PREFIX))
                undo_stack.push(
                    AddProjectTreeNodeCommand(project_tree_controller,
                                              new_node, self))

            def add_sequence_func():
                new_node = SequenceProjectTreeNode(
                    self.suggest_child_name(_SEQUENCE_NODE_NAME_PREFIX))
                undo_stack.push(
                    AddProjectTreeNodeCommand(project_tree_controller,
                                              new_node, self))

            add_menu = menu.addMenu(
                QCoreApplication.translate('BaseProjectTreeNode',
                                           '&Create Child'))
            add_dir_action = add_menu.addAction(
                DirProjectTreeNode.get_icon(),
                QCoreApplication.translate('BaseProjectTreeNode',
                                           '&Directory'))
            add_sequence_action = add_menu.addAction(
                SequenceProjectTreeNode.get_icon(),
                QCoreApplication.translate('BaseProjectTreeNode', '&Sequence'))
            add_dir_action.triggered.connect(add_dir_func)
            add_sequence_action.triggered.connect(add_sequence_func)

        # Add a menu item for deleting the node, if it is not the root node.
        if self.parent is not None:

            def delete_func():
                undo_stack.push(
                    DeleteProjectTreeNodeCommand(project_tree_controller,
                                                 self))

            delete_action = menu.addAction(
                QCoreApplication.translate('BaseProjectTreeNode', '&Delete'))
            delete_action.triggered.connect(delete_func)

        return menu
예제 #14
0
    def contextMenuEvent(self, ev):
        from calibre.gui2.open_with import populate_menu
        cm = QMenu(self)
        paste = cm.addAction(_('Paste Cover'))
        copy = cm.addAction(_('Copy Cover'))
        remove = cm.addAction(_('Remove Cover'))
        gc = cm.addAction(_('Generate Cover from metadata'))
        if not QApplication.instance().clipboard().mimeData().hasImage():
            paste.setEnabled(False)
        copy.triggered.connect(self.copy_to_clipboard)
        paste.triggered.connect(self.paste_from_clipboard)
        remove.triggered.connect(self.remove_cover)
        gc.triggered.connect(self.generate_cover)

        m = QMenu(_('Open with...'))
        populate_menu(m, self.open_with, 'jpeg')
        if len(m.actions()) == 0:
            cm.addAction(_('Open with...'), self.choose_open_with)
        else:
            m.addSeparator()
            m.addAction(_('Choose other program...'), self.choose_open_with)
            cm.addMenu(m)
        cm.exec_(ev.globalPos())
예제 #15
0
    def show_context_menu(self):
        menu = QMenu(self)
        item = self.current_item()

        if not item:
            return

        actions = list()
        file_menu = menu.addMenu(QIcon("res/delete.svg"), "File operations")
        delete_action = QAction(QIcon("res/delete.svg"), "Delete", file_menu)
        actions.append(delete_action)
        file_menu.addActions(actions)
        file_menu.setEnabled(True)
        menu.setEnabled(True)
        action = menu.exec_(QCursor.pos())
예제 #16
0
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.prefs['virt_libs_hidden']
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide virtual library tabs'), self.disable_bar)
     i = self.tabAt(ev.pos())
     if i > -1:
         vl = str(self.tabData(i) or '')
         if vl:
             m.addSeparator()
             m.addAction(_('Edit "%s"') % vl, partial(self.gui.do_create_edit, name=vl))
             m.addAction(_('Delete "%s"') % vl, partial(self.gui.remove_vl_triggered, name=vl))
     m.exec_(ev.globalPos())
예제 #17
0
파일: init.py 프로젝트: AEliu/calibre
 def contextMenuEvent(self, ev):
     m = QMenu(self)
     m.addAction(_('Sort alphabetically'), self.sort_alphabetically)
     hidden = self.current_db.prefs['virt_libs_hidden']
     if hidden:
         s = m._s = m.addMenu(_('Restore hidden tabs'))
         for x in hidden:
             s.addAction(x, partial(self.restore, x))
     m.addAction(_('Hide virtual library tabs'), self.disable_bar)
     i = self.tabAt(ev.pos())
     if i > -1:
         vl = unicode(self.tabData(i) or '')
         if vl:
             m.addSeparator()
             m.addAction(_('Edit "%s"') % vl, partial(self.gui.do_create_edit, name=vl))
             m.addAction(_('Delete "%s"') % vl, partial(self.gui.remove_vl_triggered, name=vl))
     m.exec_(ev.globalPos())
    def init_inspection_test(self):

        button = QPushButton("Robot inspection (beta)")

        menu = QMenu("Menu")
        menu.setStyleSheet("QMenu { menu-scrollable: 1; }");
        submenus = []


        for i in range(5):
            # a component has fields which are either strings or arrays.
            s = menu.addMenu(str(i))
            for j in range(5):
                s.addAction(str(j), partial(self.subscribe_to_field_test, "blubber", j))

        button.setMenu(menu)

        return button
    def init_inspection(self):

        button = QPushButton("Robot inspection (beta)")

        service_list_comps = '/meka_ros_publisher/list_components'
        service_list_fields = '/meka_ros_publisher/list_fields'
        service_req_values = '/meka_ros_publisher/request_values'
        rospy.loginfo("Waiting for %s, %s and %s", service_list_comps, service_list_fields, service_req_values)
        try:
            rospy.wait_for_service(service_list_comps, 4.0)
            rospy.wait_for_service(service_list_fields, 4.0)
            rospy.wait_for_service(service_req_values, 4.0)
        except rospy.ROSException:
            rospy.logerr("%s and/or %s did not show up. Giving up", service_list_comps, service_list_fields)
            return button
            
        self.list_comps_client = rospy.ServiceProxy(service_list_comps, ListComponents)
        self.list_fields_client = rospy.ServiceProxy(service_list_fields, ListFields)
        self.req_vals_client = rospy.ServiceProxy(service_req_values, RequestValues)
        rospy.loginfo("Found %s, %s and %s", service_list_comps, service_list_fields, service_req_values)

        # get all the components
        try:
            resp = self.list_comps_client("")
        except rospy.ServiceException:
            rospy.logerr("Could not call list_components")
            return button
            
        menu = QMenu("Menu")
        menu.setStyleSheet("QMenu { menu-scrollable: 1; }");
        submenus = []

        self.req_action = {}
        if(resp):
            for component in resp.components:
                s = menu.addMenu(component)
                self.req_action[component] = s.addAction("request fields", partial(self.request_fields, component, s))
                

        button.setMenu(menu)

        return button
예제 #20
0
파일: busy.py 프로젝트: MValaguz/MGrep
    def setup_ui(self):
        self.setWindowIcon(QIcon(I('diff.png')))
        self.stacks = st = QStackedLayout(self)
        self.busy = BusyWidget(self)
        self.w = QWidget(self)
        st.addWidget(self.busy), st.addWidget(self.w)

        self.setLayout(st)
        self.l = l = QGridLayout()
        self.w.setLayout(l)

        self.view = v = DiffView(self,
                                 show_open_in_editor=self.show_open_in_editor)
        l.addWidget(v, l.rowCount(), 0, 1, -1)

        r = l.rowCount()
        self.bp = b = QToolButton(self)
        b.setIcon(QIcon(I('back.png')))
        b.clicked.connect(partial(self.view.next_change, -1))
        b.setToolTip(_('Go to previous change') + ' [p]')
        b.setText(_('&Previous change')), b.setToolButtonStyle(
            Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 0)

        self.bn = b = QToolButton(self)
        b.setIcon(QIcon(I('forward.png')))
        b.clicked.connect(partial(self.view.next_change, 1))
        b.setToolTip(_('Go to next change') + ' [n]')
        b.setText(_('&Next change')), b.setToolButtonStyle(
            Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 1)

        self.search = s = HistoryLineEdit2(self)
        s.initialize('diff_search_history')
        l.addWidget(s, r, 2)
        s.setPlaceholderText(_('Search for text'))
        s.returnPressed.connect(partial(self.do_search, False))
        self.sbn = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-down.png')))
        b.clicked.connect(partial(self.do_search, False))
        b.setToolTip(_('Find next match'))
        b.setText(_('Next &match')), b.setToolButtonStyle(
            Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 3)
        self.sbp = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-up.png')))
        b.clicked.connect(partial(self.do_search, True))
        b.setToolTip(_('Find previous match'))
        b.setText(_('P&revious match')), b.setToolButtonStyle(
            Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 4)
        self.lb = b = QRadioButton(_('Left panel'), self)
        b.setToolTip(_('Perform search in the left panel'))
        l.addWidget(b, r, 5)
        self.rb = b = QRadioButton(_('Right panel'), self)
        b.setToolTip(_('Perform search in the right panel'))
        l.addWidget(b, r, 6)
        b.setChecked(True)
        self.pb = b = QToolButton(self)
        b.setIcon(QIcon(I('config.png')))
        b.setText(_('&Options')), b.setToolButtonStyle(
            Qt.ToolButtonTextBesideIcon)
        b.setToolTip(_('Change how the differences are displayed'))
        b.setPopupMode(b.InstantPopup)
        m = QMenu(b)
        b.setMenu(m)
        cm = self.cm = QMenu(_('Lines of context around each change'))
        for i in (3, 5, 10, 50):
            cm.addAction(
                _('Show %d lines of context') % i,
                partial(self.change_context, i))
        cm.addAction(_('Show all text'), partial(self.change_context, None))
        self.beautify_action = m.addAction('', self.toggle_beautify)
        self.set_beautify_action_text()
        m.addMenu(cm)
        l.addWidget(b, r, 7)

        self.hl = QHBoxLayout()
        l.addLayout(self.hl, l.rowCount(), 0, 1, -1)
        self.names = QLabel('')
        self.hl.addWidget(self.names, r)

        self.bb.setStandardButtons(self.bb.Close)
        if self.revert_button_msg is not None:
            self.rvb = b = self.bb.addButton(self.revert_button_msg,
                                             self.bb.ActionRole)
            b.setIcon(QIcon(I('edit-undo.png'))), b.setAutoDefault(False)
            b.clicked.connect(self.revert_requested)
            b.clicked.connect(self.reject)
        self.bb.button(self.bb.Close).setDefault(True)
        self.hl.addWidget(self.bb, r)

        self.view.setFocus(Qt.OtherFocusReason)
예제 #21
0
class BooksView(QTableView):  # {{{

    files_dropped = pyqtSignal(object)
    add_column_signal = pyqtSignal()
    is_library_view = True

    def viewportEvent(self, event):
        if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']):
            return False
        return QTableView.viewportEvent(self, event)

    def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
        QTableView.__init__(self, parent)
        self.default_row_height = self.verticalHeader().defaultSectionSize()
        self.gui = parent
        self.setProperty('highlight_current_item', 150)
        self.row_sizing_done = False
        self.alternate_views = AlternateViews(self)

        if not tweaks['horizontal_scrolling_per_column']:
            self.setHorizontalScrollMode(self.ScrollPerPixel)

        self.setEditTriggers(self.EditKeyPressed)
        if tweaks['doubleclick_on_library_view'] == 'edit_cell':
            self.setEditTriggers(self.DoubleClicked|self.editTriggers())
        elif tweaks['doubleclick_on_library_view'] == 'open_viewer':
            self.setEditTriggers(self.SelectedClicked|self.editTriggers())
            self.doubleClicked.connect(parent.iactions['View'].view_triggered)
        elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
            # Must not enable single-click to edit, or the field will remain
            # open in edit mode underneath the edit metadata dialog
            if use_edit_metadata_dialog:
                self.doubleClicked.connect(
                        partial(parent.iactions['Edit Metadata'].edit_metadata,
                                checked=False))
            else:
                self.setEditTriggers(self.DoubleClicked|self.editTriggers())

        setup_dnd_interface(self)
        self.setAlternatingRowColors(True)
        self.setShowGrid(False)
        self.setWordWrap(False)

        self.rating_delegate = RatingDelegate(self)
        self.timestamp_delegate = DateDelegate(self)
        self.pubdate_delegate = PubDateDelegate(self)
        self.last_modified_delegate = DateDelegate(self,
                tweak_name='gui_last_modified_display_format')
        self.languages_delegate = LanguagesDelegate(self)
        self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names')
        self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
        self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True)
        self.series_delegate = TextDelegate(self)
        self.publisher_delegate = TextDelegate(self)
        self.text_delegate = TextDelegate(self)
        self.cc_text_delegate = CcTextDelegate(self)
        self.cc_enum_delegate = CcEnumDelegate(self)
        self.cc_bool_delegate = CcBoolDelegate(self)
        self.cc_comments_delegate = CcCommentsDelegate(self)
        self.cc_template_delegate = CcTemplateDelegate(self)
        self.cc_number_delegate = CcNumberDelegate(self)
        self.display_parent = parent
        self._model = modelcls(self)
        self.setModel(self._model)
        self._model.count_changed_signal.connect(self.do_row_sizing,
                                                 type=Qt.QueuedConnection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setSortingEnabled(True)
        self.selectionModel().currentRowChanged.connect(self._model.current_changed)
        self.preserve_state = partial(PreserveViewState, self)
        self.marked_changed_listener = FunctionDispatcher(self.marked_changed)

        # {{{ Column Header setup
        self.can_add_columns = True
        self.was_restored = False
        self.column_header = HeaderView(Qt.Horizontal, self)
        self.setHorizontalHeader(self.column_header)
        self.column_header.sortIndicatorChanged.disconnect()
        self.column_header.sortIndicatorChanged.connect(self.user_sort_requested)
        self.column_header.setSectionsMovable(True)
        self.column_header.setSectionsClickable(True)
        self.column_header.sectionMoved.connect(self.save_state)
        self.column_header.setContextMenuPolicy(Qt.CustomContextMenu)
        self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu)
        self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection)
        self.row_header = HeaderView(Qt.Vertical, self)
        self.row_header.setSectionResizeMode(self.row_header.Fixed)
        self.setVerticalHeader(self.row_header)
        # }}}

        self._model.database_changed.connect(self.database_changed)
        hv = self.verticalHeader()
        hv.setSectionsClickable(True)
        hv.setCursor(Qt.PointingHandCursor)
        self.selected_ids = []
        self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
        self._model.sorting_done.connect(self.sorting_done,
                type=Qt.QueuedConnection)

    # Column Header Context Menu {{{
    def column_header_context_handler(self, action=None, column=None):
        if not action or not column:
            return
        try:
            idx = self.column_map.index(column)
        except:
            return
        h = self.column_header

        if action == 'hide':
            if h.hiddenSectionCount() >= h.count():
                return error_dialog(self, _('Cannot hide all columns'), _(
                    'You must not hide all columns'), show=True)
            h.setSectionHidden(idx, True)
        elif action == 'show':
            h.setSectionHidden(idx, False)
            if h.sectionSize(idx) < 3:
                sz = h.sectionSizeHint(idx)
                h.resizeSection(idx, sz)
        elif action == 'ascending':
            self.sort_by_column_and_order(idx, True)
        elif action == 'descending':
            self.sort_by_column_and_order(idx, False)
        elif action == 'defaults':
            self.apply_state(self.get_default_state())
        elif action == 'addcustcol':
            self.add_column_signal.emit()
        elif action.startswith('align_'):
            alignment = action.partition('_')[-1]
            self._model.change_alignment(column, alignment)
        elif action == 'quickview':
            from calibre.customize.ui import find_plugin
            qv = find_plugin('Show Quickview')
            if qv:
                rows = self.selectionModel().selectedRows()
                if len(rows) > 0:
                    current_row = rows[0].row()
                    current_col = self.column_map.index(column)
                    index = self.model().index(current_row, current_col)
                    qv.actual_plugin_.change_quickview_column(index)

        self.save_state()

    def show_column_header_context_menu(self, pos):
        idx = self.column_header.logicalIndexAt(pos)
        if idx > -1 and idx < len(self.column_map):
            col = self.column_map[idx]
            name = unicode(self.model().headerData(idx, Qt.Horizontal,
                    Qt.DisplayRole) or '')
            self.column_header_context_menu = QMenu(self)
            if col != 'ondevice':
                self.column_header_context_menu.addAction(_('Hide column %s') %
                        name,
                    partial(self.column_header_context_handler, action='hide',
                        column=col))
            m = self.column_header_context_menu.addMenu(
                    _('Sort on %s')  % name)
            a = m.addAction(_('Ascending'),
                    partial(self.column_header_context_handler,
                        action='ascending', column=col))
            d = m.addAction(_('Descending'),
                    partial(self.column_header_context_handler,
                        action='descending', column=col))
            if self._model.sorted_on[0] == col:
                ac = a if self._model.sorted_on[1] else d
                ac.setCheckable(True)
                ac.setChecked(True)
            if col not in ('ondevice', 'inlibrary') and \
                    (not self.model().is_custom_column(col) or
                    self.model().custom_columns[col]['datatype'] not in ('bool',
                        )):
                m = self.column_header_context_menu.addMenu(
                        _('Change text alignment for %s') % name)
                al = self._model.alignment_map.get(col, 'left')
                for x, t in (('left', _('Left')), ('right', _('Right')), ('center',
                    _('Center'))):
                        a = m.addAction(t,
                            partial(self.column_header_context_handler,
                            action='align_'+x, column=col))
                        if al == x:
                            a.setCheckable(True)
                            a.setChecked(True)

            if not isinstance(self, DeviceBooksView):
                if self._model.db.field_metadata[col]['is_category']:
                    act = self.column_header_context_menu.addAction(_('Quickview column %s') %
                            name,
                        partial(self.column_header_context_handler, action='quickview',
                            column=col))
                    rows = self.selectionModel().selectedRows()
                    if len(rows) > 1:
                        act.setEnabled(False)

            hidden_cols = [self.column_map[i] for i in
                    range(self.column_header.count()) if
                    self.column_header.isSectionHidden(i)]
            try:
                hidden_cols.remove('ondevice')
            except:
                pass
            if hidden_cols:
                self.column_header_context_menu.addSeparator()
                m = self.column_header_context_menu.addMenu(_('Show column'))
                for col in hidden_cols:
                    hidx = self.column_map.index(col)
                    name = unicode(self.model().headerData(hidx, Qt.Horizontal,
                            Qt.DisplayRole) or '')
                    m.addAction(name,
                        partial(self.column_header_context_handler,
                        action='show', column=col))

            self.column_header_context_menu.addSeparator()
            self.column_header_context_menu.addAction(
                    _('Shrink column if it is too wide to fit'),
                    partial(self.resize_column_to_fit, column=self.column_map[idx]))
            self.column_header_context_menu.addAction(
                    _('Restore default layout'),
                    partial(self.column_header_context_handler,
                        action='defaults', column=col))

            if self.can_add_columns:
                self.column_header_context_menu.addAction(
                        QIcon(I('column.png')),
                        _('Add your own columns'),
                        partial(self.column_header_context_handler,
                            action='addcustcol', column=col))

            self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos))
    # }}}

    # Sorting {{{
    def sort_by_column_and_order(self, col, ascending):
        self.column_header.blockSignals(True)
        self.sortByColumn(col, Qt.AscendingOrder if ascending else Qt.DescendingOrder)
        self.column_header.blockSignals(False)

    def user_sort_requested(self, col, order=Qt.AscendingOrder):
        if col >= len(self.column_map) or col < 0:
            return QTableView.sortByColumn(self, col)
        field = self.column_map[col]
        self.intelligent_sort(field, order == Qt.AscendingOrder)

    def intelligent_sort(self, field, ascending):
        m = self.model()
        pname = 'previous_sort_order_' + self.__class__.__name__
        previous = gprefs.get(pname, {})
        if field == m.sorted_on[0] or field not in previous:
            self.sort_by_named_field(field, ascending)
            previous[field] = ascending
            gprefs[pname] = previous
            return
        previous[m.sorted_on[0]] = m.sorted_on[1]
        gprefs[pname] = previous
        self.sort_by_named_field(field, previous[field])

    def about_to_be_sorted(self, idc):
        selected_rows = [r.row() for r in self.selectionModel().selectedRows()]
        self.selected_ids = [idc(r) for r in selected_rows]

    def sorting_done(self, indexc):
        pos = self.horizontalScrollBar().value()
        self.select_rows(self.selected_ids, using_ids=True, change_current=True,
            scroll=True)
        self.selected_ids = []
        self.horizontalScrollBar().setValue(pos)

    def sort_by_named_field(self, field, order, reset=True):
        if field in self.column_map:
            idx = self.column_map.index(field)
            self.sort_by_column_and_order(idx, order)
        else:
            self._model.sort_by_named_field(field, order, reset)
            self.column_header.blockSignals(True)
            self.column_header.setSortIndicator(-1, Qt.AscendingOrder)
            self.column_header.blockSignals(False)

    def multisort(self, fields, reset=True, only_if_different=False):
        if len(fields) == 0:
            return
        sh = self.cleanup_sort_history(self._model.sort_history,
                                       ignore_column_map=True)
        if only_if_different and len(sh) >= len(fields):
            ret=True
            for i,t in enumerate(fields):
                if t[0] != sh[i][0]:
                    ret = False
                    break
            if ret:
                return

        for n,d in reversed(fields):
            if n in self._model.db.field_metadata.keys():
                sh.insert(0, (n, d))
        sh = self.cleanup_sort_history(sh, ignore_column_map=True)
        self._model.sort_history = [tuple(x) for x in sh]
        self._model.resort(reset=reset)
        col = fields[0][0]
        dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder
        if col in self.column_map:
            col = self.column_map.index(col)
            self.column_header.blockSignals(True)
            try:
                self.column_header.setSortIndicator(col, dir)
            finally:
                self.column_header.blockSignals(False)
    # }}}

    # Ondevice column {{{
    def set_ondevice_column_visibility(self):
        col, h = self._model.column_map.index('ondevice'), self.column_header
        h.setSectionHidden(col, not self._model.device_connected)

    def set_device_connected(self, is_connected):
        self._model.set_device_connected(is_connected)
        self.set_ondevice_column_visibility()
    # }}}

    # Save/Restore State {{{
    def get_state(self):
        h = self.column_header
        cm = self.column_map
        state = {}
        state['hidden_columns'] = [cm[i] for i in range(h.count())
                if h.isSectionHidden(i) and cm[i] != 'ondevice']
        state['last_modified_injected'] = True
        state['languages_injected'] = True
        state['sort_history'] = \
            self.cleanup_sort_history(self.model().sort_history, ignore_column_map=self.is_library_view)
        state['column_positions'] = {}
        state['column_sizes'] = {}
        state['column_alignment'] = self._model.alignment_map
        for i in range(h.count()):
            name = cm[i]
            state['column_positions'][name] = h.visualIndex(i)
            if name != 'ondevice':
                state['column_sizes'][name] = h.sectionSize(i)
        return state

    def write_state(self, state):
        db = getattr(self.model(), 'db', None)
        name = unicode(self.objectName())
        if name and db is not None:
            db.new_api.set_pref(name + ' books view state', state)

    def save_state(self):
        # Only save if we have been initialized (set_database called)
        if len(self.column_map) > 0 and self.was_restored:
            state = self.get_state()
            self.write_state(state)

    def cleanup_sort_history(self, sort_history, ignore_column_map=False):
        history = []

        for col, order in sort_history:
            if not isinstance(order, bool):
                continue
            col = {'date':'timestamp', 'sort':'title'}.get(col, col)
            if ignore_column_map or col in self.column_map:
                if (not history or history[-1][0] != col):
                    history.append([col, order])
        return history

    def apply_sort_history(self, saved_history, max_sort_levels=3):
        if not saved_history:
            return
        if self.is_library_view:
            for col, order in reversed(self.cleanup_sort_history(
                    saved_history, ignore_column_map=True)[:max_sort_levels]):
                self.sort_by_named_field(col, order)
        else:
            for col, order in reversed(self.cleanup_sort_history(
                    saved_history)[:max_sort_levels]):
                self.sort_by_column_and_order(self.column_map.index(col), order)

    def apply_state(self, state, max_sort_levels=3):
        h = self.column_header
        cmap = {}
        hidden = state.get('hidden_columns', [])
        for i, c in enumerate(self.column_map):
            cmap[c] = i
            if c != 'ondevice':
                h.setSectionHidden(i, c in hidden)

        positions = state.get('column_positions', {})
        pmap = {}
        for col, pos in positions.items():
            if col in cmap:
                pmap[pos] = col
        for pos in sorted(pmap.keys()):
            col = pmap[pos]
            idx = cmap[col]
            current_pos = h.visualIndex(idx)
            if current_pos != pos:
                h.moveSection(current_pos, pos)

        # Because of a bug in Qt 5 we have to ensure that the header is actually
        # relaid out by changing this value, without this sometimes ghost
        # columns remain visible when changing libraries
        for i in xrange(h.count()):
            val = h.isSectionHidden(i)
            h.setSectionHidden(i, not val)
            h.setSectionHidden(i, val)

        sizes = state.get('column_sizes', {})
        for col, size in sizes.items():
            if col in cmap:
                sz = sizes[col]
                if sz < 3:
                    sz = h.sectionSizeHint(cmap[col])
                h.resizeSection(cmap[col], sz)

        self.apply_sort_history(state.get('sort_history', None),
                max_sort_levels=max_sort_levels)

        for col, alignment in state.get('column_alignment', {}).items():
            self._model.change_alignment(col, alignment)

        for i in range(h.count()):
            if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
                sz = h.sectionSizeHint(i)
                h.resizeSection(i, sz)

    def get_default_state(self):
        old_state = {
                'hidden_columns': ['last_modified', 'languages'],
                'sort_history':[DEFAULT_SORT],
                'column_positions': {},
                'column_sizes': {},
                'column_alignment': {
                    'size':'center',
                    'timestamp':'center',
                    'pubdate':'center'},
                'last_modified_injected': True,
                'languages_injected': True,
                }
        h = self.column_header
        cm = self.column_map
        for i in range(h.count()):
            name = cm[i]
            old_state['column_positions'][name] = i
            if name != 'ondevice':
                old_state['column_sizes'][name] = \
                    min(350, max(self.sizeHintForColumn(i),
                        h.sectionSizeHint(i)))
                if name in ('timestamp', 'last_modified'):
                    old_state['column_sizes'][name] += 12
        return old_state

    def get_old_state(self):
        ans = None
        name = unicode(self.objectName())
        if name:
            name += ' books view state'
            db = getattr(self.model(), 'db', None)
            if db is not None:
                ans = db.prefs.get(name, None)
                if ans is None:
                    ans = gprefs.get(name, None)
                    try:
                        del gprefs[name]
                    except:
                        pass
                    if ans is not None:
                        db.new_api.set_pref(name, ans)
                else:
                    injected = False
                    if not ans.get('last_modified_injected', False):
                        injected = True
                        ans['last_modified_injected'] = True
                        hc = ans.get('hidden_columns', [])
                        if 'last_modified' not in hc:
                            hc.append('last_modified')
                    if not ans.get('languages_injected', False):
                        injected = True
                        ans['languages_injected'] = True
                        hc = ans.get('hidden_columns', [])
                        if 'languages' not in hc:
                            hc.append('languages')
                    if injected:
                        db.new_api.set_pref(name, ans)
        return ans

    def restore_state(self):
        old_state = self.get_old_state()
        if old_state is None:
            old_state = self.get_default_state()
        max_levels = 3

        if tweaks['sort_columns_at_startup'] is not None:
            sh = []
            try:
                for c,d in tweaks['sort_columns_at_startup']:
                    if not isinstance(d, bool):
                        d = True if d == 0 else False
                    sh.append((c, d))
            except:
                # Ignore invalid tweak values as users seem to often get them
                # wrong
                print('Ignoring invalid sort_columns_at_startup tweak, with error:')
                import traceback
                traceback.print_exc()
            old_state['sort_history'] = sh
            max_levels = max(3, len(sh))

        self.column_header.blockSignals(True)
        self.apply_state(old_state, max_sort_levels=max_levels)
        self.column_header.blockSignals(False)

        self.do_row_sizing()

        self.was_restored = True

    def refresh_row_sizing(self):
        self.row_sizing_done = False
        self.do_row_sizing()

    def do_row_sizing(self):
        # Resize all rows to have the correct height
        if not self.row_sizing_done and self.model().rowCount(QModelIndex()) > 0:
            vh = self.verticalHeader()
            vh.setDefaultSectionSize(max(vh.minimumSectionSize(), self.default_row_height + gprefs['book_list_extra_row_spacing']))
            self._model.set_row_height(self.rowHeight(0))
            self.row_sizing_done = True

    def resize_column_to_fit(self, column):
        col = self.column_map.index(column)
        self.column_resized(col, self.columnWidth(col), self.columnWidth(col))

    def column_resized(self, col, old_size, new_size):
        # arbitrary: scroll bar + header + some
        max_width = self.width() - (self.verticalScrollBar().width() +
                                    self.verticalHeader().width() + 10)
        if max_width < 200:
            max_width = 200
        if new_size > max_width:
            self.column_header.blockSignals(True)
            self.setColumnWidth(col, max_width)
            self.column_header.blockSignals(False)

    # }}}

    # Initialization/Delegate Setup {{{

    def set_database(self, db):
        self.alternate_views.set_database(db)
        self.save_state()
        self._model.set_database(db)
        self.tags_delegate.set_database(db)
        self.cc_names_delegate.set_database(db)
        self.authors_delegate.set_database(db)
        self.series_delegate.set_auto_complete_function(db.all_series)
        self.publisher_delegate.set_auto_complete_function(db.all_publishers)
        self.alternate_views.set_database(db, stage=1)

    def marked_changed(self, old_marked, current_marked):
        self.alternate_views.marked_changed(old_marked, current_marked)
        if bool(old_marked) == bool(current_marked):
            changed = old_marked | current_marked
            i = self.model().db.data.id_to_index
            def f(x):
                try:
                    return i(x)
                except ValueError:
                    pass
            sections = tuple(x for x in map(f, changed) if x is not None)
            if sections:
                self.row_header.headerDataChanged(Qt.Vertical, min(sections), max(sections))
                # This is needed otherwise Qt does not always update the
                # viewport correctly. See https://bugs.launchpad.net/bugs/1404697
                self.row_header.viewport().update()
        else:
            # Marked items have either appeared or all been removed
            self.model().set_row_decoration(current_marked)
            self.row_header.headerDataChanged(Qt.Vertical, 0, self.row_header.count()-1)
            self.row_header.geometriesChanged.emit()

    def database_changed(self, db):
        db.data.add_marked_listener(self.marked_changed_listener)
        for i in range(self.model().columnCount(None)):
            if self.itemDelegateForColumn(i) in (self.rating_delegate,
                    self.timestamp_delegate, self.pubdate_delegate,
                    self.last_modified_delegate, self.languages_delegate):
                self.setItemDelegateForColumn(i, self.itemDelegate())

        cm = self.column_map

        for colhead in cm:
            if self._model.is_custom_column(colhead):
                cc = self._model.custom_columns[colhead]
                if cc['datatype'] == 'datetime':
                    delegate = CcDateDelegate(self)
                    delegate.set_format(cc['display'].get('date_format',''))
                    self.setItemDelegateForColumn(cm.index(colhead), delegate)
                elif cc['datatype'] == 'comments':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
                elif cc['datatype'] == 'text':
                    if cc['is_multiple']:
                        if cc['display'].get('is_names', False):
                            self.setItemDelegateForColumn(cm.index(colhead),
                                                          self.cc_names_delegate)
                        else:
                            self.setItemDelegateForColumn(cm.index(colhead),
                                                          self.tags_delegate)
                    else:
                        self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
                elif cc['datatype'] == 'series':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
                elif cc['datatype'] in ('int', 'float'):
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_number_delegate)
                elif cc['datatype'] == 'bool':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
                elif cc['datatype'] == 'rating':
                    self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
                elif cc['datatype'] == 'composite':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
                elif cc['datatype'] == 'enumeration':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate)
            else:
                dattr = colhead+'_delegate'
                delegate = colhead if hasattr(self, dattr) else 'text'
                self.setItemDelegateForColumn(cm.index(colhead), getattr(self,
                    delegate+'_delegate'))

        self.restore_state()
        self.set_ondevice_column_visibility()
        # incase there were marked books
        self.model().set_row_decoration(set())
        self.row_header.headerDataChanged(Qt.Vertical, 0, self.row_header.count()-1)
        self.row_header.geometriesChanged.emit()
        # }}}

    # Context Menu {{{
    def set_context_menu(self, menu, edit_collections_action):
        self.setContextMenuPolicy(Qt.DefaultContextMenu)
        self.context_menu = menu
        self.alternate_views.set_context_menu(menu)
        self.edit_collections_action = edit_collections_action

    def contextMenuEvent(self, event):
        sac = self.gui.iactions['Sort By']
        sort_added = tuple(ac for ac in self.context_menu.actions() if ac is sac.qaction)
        if sort_added:
            sac.update_menu()
        self.context_menu.popup(event.globalPos())
        event.accept()
    # }}}

    @property
    def column_map(self):
        return self._model.column_map

    @property
    def visible_columns(self):
        h = self.horizontalHeader()
        logical_indices = (x for x in xrange(h.count()) if not h.isSectionHidden(x))
        rmap = {i:x for i, x in enumerate(self.column_map)}
        return (rmap[h.visualIndex(x)] for x in logical_indices if h.visualIndex(x) > -1)

    def refresh_book_details(self):
        idx = self.currentIndex()
        if idx.isValid():
            self._model.current_changed(idx, idx)
            return True
        return False

    def scrollContentsBy(self, dx, dy):
        # Needed as Qt bug causes headerview to not always update when scrolling
        QTableView.scrollContentsBy(self, dx, dy)
        if dy != 0:
            self.column_header.update()

    def scroll_to_row(self, row):
        if row > -1:
            h = self.horizontalHeader()
            for i in range(h.count()):
                if not h.isSectionHidden(i) and h.sectionViewportPosition(i) >= 0:
                    self.scrollTo(self.model().index(row, i), self.PositionAtCenter)
                    break

    @property
    def current_book(self):
        ci = self.currentIndex()
        if ci.isValid():
            try:
                return self.model().db.data.index_to_id(ci.row())
            except (IndexError, ValueError, KeyError, TypeError, AttributeError):
                pass

    def current_book_state(self):
        return self.current_book, self.horizontalScrollBar().value()

    def restore_current_book_state(self, state):
        book_id, hpos = state
        try:
            row = self.model().db.data.id_to_index(book_id)
        except (IndexError, ValueError, KeyError, TypeError, AttributeError):
            return
        self.set_current_row(row)
        self.scroll_to_row(row)
        self.horizontalScrollBar().setValue(hpos)

    def set_current_row(self, row=0, select=True, for_sync=False):
        if row > -1 and row < self.model().rowCount(QModelIndex()):
            h = self.horizontalHeader()
            logical_indices = list(range(h.count()))
            logical_indices = [x for x in logical_indices if not
                    h.isSectionHidden(x)]
            pairs = [(x, h.visualIndex(x)) for x in logical_indices if
                    h.visualIndex(x) > -1]
            if not pairs:
                pairs = [(0, 0)]
            pairs.sort(cmp=lambda x,y:cmp(x[1], y[1]))
            i = pairs[0][0]
            index = self.model().index(row, i)
            if for_sync:
                sm = self.selectionModel()
                sm.setCurrentIndex(index, sm.NoUpdate)
            else:
                self.setCurrentIndex(index)
                if select:
                    sm = self.selectionModel()
                    sm.select(index, sm.ClearAndSelect|sm.Rows)

    def row_at_top(self):
        pos = 0
        while pos < 100:
            ans = self.rowAt(pos)
            if ans > -1:
                return ans
            pos += 5

    def row_at_bottom(self):
        pos = self.viewport().height()
        limit = pos - 100
        while pos > limit:
            ans = self.rowAt(pos)
            if ans > -1:
                return ans
            pos -= 5

    def moveCursor(self, action, modifiers):
        orig = self.currentIndex()
        index = QTableView.moveCursor(self, action, modifiers)
        if action == QTableView.MovePageDown:
            moved = index.row() - orig.row()
            try:
                rows = self.row_at_bottom() - self.row_at_top()
            except TypeError:
                rows = moved
            if moved > rows:
                index = self.model().index(orig.row() + rows, index.column())
        elif action == QTableView.MovePageUp:
            moved = orig.row() - index.row()
            try:
                rows = self.row_at_bottom() - self.row_at_top()
            except TypeError:
                rows = moved
            if moved > rows:
                index = self.model().index(orig.row() - rows, index.column())
        elif action == QTableView.MoveHome and modifiers & Qt.ControlModifier:
            return self.model().index(0, orig.column())
        elif action == QTableView.MoveEnd and modifiers & Qt.ControlModifier:
            return self.model().index(self.model().rowCount(QModelIndex()) - 1, orig.column())
        return index

    def selectionCommand(self, index, event):
        if event and event.type() == event.KeyPress and event.key() in (Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL:
            return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
        return super(BooksView, self).selectionCommand(index, event)

    def ids_to_rows(self, ids):
        row_map = OrderedDict()
        ids = frozenset(ids)
        m = self.model()
        for row in xrange(m.rowCount(QModelIndex())):
            if len(row_map) >= len(ids):
                break
            c = m.id(row)
            if c in ids:
                row_map[c] = row
        return row_map

    def select_rows(self, identifiers, using_ids=True, change_current=True,
            scroll=True):
        '''
        Select rows identified by identifiers. identifiers can be a set of ids,
        row numbers or QModelIndexes.
        '''
        rows = set([x.row() if hasattr(x, 'row') else x for x in
            identifiers])
        if using_ids:
            rows = set([])
            identifiers = set(identifiers)
            m = self.model()
            for row in xrange(m.rowCount(QModelIndex())):
                if m.id(row) in identifiers:
                    rows.add(row)
        rows = list(sorted(rows))
        if rows:
            row = rows[0]
            if change_current:
                self.set_current_row(row, select=False)
            if scroll:
                self.scroll_to_row(row)
        sm = self.selectionModel()
        sel = QItemSelection()
        m = self.model()
        max_col = m.columnCount(QModelIndex()) - 1
        # Create a range based selector for each set of contiguous rows
        # as supplying selectors for each individual row causes very poor
        # performance if a large number of rows has to be selected.
        for k, g in itertools.groupby(enumerate(rows), lambda (i,x):i-x):
            group = list(map(operator.itemgetter(1), g))
            sel.merge(QItemSelection(m.index(min(group), 0),
                m.index(max(group), max_col)), sm.Select)
        sm.select(sel, sm.ClearAndSelect)
예제 #22
0
    def menuHeros(self,event):
        #====== Menu Move ==========
        print ('context menu event de heros')
        menu_move = QMenu("Move")
        moveNormal = QAction("Normal",None)
        moveNormal.setData(1.0)
        moveNormal.triggered.connect(self.onActionMoveNormal)
        menu_move.addAction(moveNormal)
        moveSlow= QAction("Slow",None)
        moveSlow.setData(0.5)
        moveSlow.triggered.connect(self.onActionMoveNormal)
        menu_move.addAction(moveSlow)
#         moveVerySlow= QAction("Very Slow",None)
#         moveVerySlow.setData(0.25)
#         moveVerySlow.triggered.connect(self.onActionMove)
#        menu_move.addAction(moveVerySlow)
        moveFast= QAction("Fast",None)
        moveFast.setData(2.0)
        moveFast.triggered.connect(self.onActionMoveNormal)
        menu_move.addAction(moveFast)
#         moveVeryFast= QAction("Very Fast",None)
#         moveVeryFast.setData(4.0)
#         moveVeryFast.triggered.connect(self.onActionMove)
#         menu_move.addAction(moveVeryFast)
        actionTeleport= QAction("Teleport",None)
        actionTeleport.setData(0.0)
        actionTeleport.triggered.connect(self.onActionMoveNormal)
        menu_move.addAction(actionTeleport)        
        #========== Menu Actions ========
        menu_actions = QMenu("Action")
        actionAttack= QAction("Attack",None)
        menu_actions.addAction(actionAttack)
        actionHeal= QAction("Soigne",None)
        menu_actions.addAction(actionHeal)        
        
        #======== MENU GENERAL ================
        menu = QMenu()
#         ok = True
#         for w in self.univers.selectedWarriors():
#             for action in self.univers.list_actions.values():
#                 if action.LeftPartContainHeros(w.id):
#                     ok = False
#                     break
#         if ok == True : 
        action_running = False
        for w in self.univers.selectedWarriors():
            if w.attribs['status']!= "repos":
                print ("mmmmmmmmm",w.attribs['status'])
                action_running = True

        if action_running == True :
            actionCancel= QAction("Cancel",None)
            actionCancel.triggered.connect(self.onActionCancel)
            menu.addAction(actionCancel)            
        else:
            menu.addMenu(menu_move)
            menu.addMenu(menu_actions)
            actionPlacement= QAction("Placement",None)
            actionPlacement.triggered.connect(self.onActionPlacement)
            actionPlacement.setData(0.0)  # en placement on teleport forcement
            menu.addAction(actionPlacement)

                

                
        if len(self.univers.selectedWarriors())==1 : 
            if self.univers.selectedWarriors()[0].attribs['HP']== 0:
                actionRebirth= QAction("Rebirth",None)
                menu.addAction(actionRebirth)  
            else:
                actionKill= QAction("Kill",None)
                menu.addAction(actionKill)
                    
        #menu.exec_(event.screenPos())
        #event.accept()
        menu.exec_(event.globalPos())
예제 #23
0
class Widget(QWidget, ScreenWidget):
    name = "manualPartitioning"

    def __init__(self):
        QWidget.__init__(self)
        self.ui = Ui_ManualPartWidget()
        self.ui.setupUi(self)
        self.storage = ctx.storage
        self.intf = ctx.interface

        # New Device Popup Menu
        self.setupMenu()

        #self.ui.newButtonclicked.connect(self.createDevice)
        self.ui.editButton.clicked.connect(self.editDevice)
        self.ui.deleteButton.clicked.connect(self.deleteDevice)
        self.ui.resetButton.clicked.connect(self.reset)
        self.ui.deviceTree.itemClicked[QTreeWidgetItem,
                                       int].connect(self.activateButtons)
        self.menu.triggered[QAction].connect(self.createDevice)

    def shown(self):
        checkForSwapNoMatch(self.intf, self.storage)
        self.populate()
        (errors, warnings) = self.storage.sanityCheck()
        if errors or warnings:
            ctx.mainScreen.disableNext()
        else:
            ctx.mainScreen.enableNext()
        self.update()

    def execute(self):
        ctx.logger.info("Manual Partitioning selected...")
        ctx.mainScreen.processEvents()
        check = self.nextCheck()
        if not check:
            ctx.mainScreen.enableBack()
        elif check is None:
            ctx.mainScreen.enableNext()
        return check

    def update(self):
        if self.storage.storageset.rootDevice:
            ctx.mainScreen.enableNext()
        else:
            ctx.mainScreen.disableNext()
        self.updateMenus()

    def activateButtons(self, item, index):
        if item:
            if isinstance(item.device, Device) and not isinstance(
                    item.device, parted.partition.Partition):
                self.ui.editButton.setEnabled(True)
                self.ui.deleteButton.setEnabled(True)
            else:
                self.ui.editButton.setEnabled(False)
                self.ui.deleteButton.setEnabled(False)

    def nextCheck(self):
        (errors, warnings) = self.storage.sanityCheck()
        if errors:
            detailed =  _("The partitioning scheme you requested "
                          "caused the following critical errors."
                          "You must correct these errors before "
                          "you continue your installation of %s.") \
                         % yali.util.product_name()

            comments = "\n\n".join(errors)
            self.intf.detailedMessageWindow(_("Partitioning Errors"),
                                            detailed,
                                            comments,
                                            type="error")
            return False

        if warnings:
            detailed = _("The partitioning scheme you requested generated the "
                         "following warnings. Would you like to continue with "
                         "your requested partitioning "
                         "scheme?")

            comments = "\n\n".join(warnings)
            rc = self.intf.detailedMessageWindow(
                _("Partitioning Warnings"),
                detailed,
                comments,
                type="custom",
                customIcon="warning",
                customButtons=[_("Ok"), _("Cancel")],
                default=1)
            if rc:
                return False

        formatWarnings = getPreExistFormatWarnings(self.storage)
        if formatWarnings:
            detailed = _("The following pre-existing devices have "
                         "been selected to be formatted, destroying "
                         "all data.")

            comments = ""
            for (device, type, mountpoint) in formatWarnings:
                comments = comments + "%s         %s         %s\n" % (
                    device, type, mountpoint)

            rc = self.intf.detailedMessageWindow(
                _("Format Warnings"),
                detailed,
                comments,
                type="custom",
                customIcon="warning",
                customButtons=[_("Format"), _("Cancel")],
                default=1)
            if rc:
                return False

        return True

    def backCheck(self):
        rc = self.intf.messageWindow(
            _("Warning"),
            _("All Changes that you made will be removed"),
            type="custom",
            customIcon="warning",
            customButtons=[_("Ok"), _("Cancel")],
            default=1)
        if not rc:
            self.storage.reset()
            return True
        return False

    def setupMenu(self):
        self.menu = QMenu("New")
        self.standardDevices = self.menu.addMenu(_("Standard"))
        self.lvmDevices = self.menu.addMenu(_("LVM"))
        self.raidDevices = self.menu.addMenu(_("RAID"))

        self.createPartition = self.standardDevices.addAction(_("Partition"))
        self.createPartition.setWhatsThis(
            _("General purpose of partition creation"))
        self.createPartition.setVisible(False)
        self.createPhysicalVolume = self.lvmDevices.addAction(
            _("Physical Volume"))
        self.createPhysicalVolume.setWhatsThis(
            _("Create LVM formatted partition"))
        self.createPhysicalVolume.setVisible(False)
        self.createVolumeGroup = self.lvmDevices.addAction(_("Volume Group"))
        self.createVolumeGroup.setWhatsThis(
            _("Requires at least 1 free LVM formatted partition"))
        self.createVolumeGroup.setVisible(False)
        self.createLogicalVolume = self.lvmDevices.addAction(
            _("Logical Volume"))
        self.createLogicalVolume.setWhatsThis(
            _("Create Logical Volume on selected Volume Group"))
        self.createLogicalVolume.setVisible(False)
        self.createRaidMember = self.raidDevices.addAction(_("Member"))
        self.createRaidMember.setWhatsThis(
            _("Create Raid formatted partition"))
        self.createRaidMember.setVisible(False)
        self.createRaidArray = self.raidDevices.addAction(_("Array"))
        self.createRaidArray.setWhatsThis(
            _("Requires at least 2 free Raid formatted partition"))
        self.createRaidArray.setVisible(False)

        self.ui.newButton.setMenu(self.menu)

    def addDevice(self, device, item):
        if device.format.hidden:
            return

        format = device.format

        if not format.exists:
            formatIcon = QIcon(":/gui/pics/tick.png")
        else:
            #formatIcon = QIcon(":/gui/pics/dialog-error.png")
            formatIcon = QIcon("")

        # mount point string
        if format.type == "lvmpv":
            vg = None
            for _vg in self.storage.vgs:
                if _vg.dependsOn(device):
                    vg = _vg
                    break
            mountpoint = getattr(vg, "name", "")
        elif format.type == "mdmember":
            array = None
            for _array in self.storage.raidArrays:
                if _array.dependsOn(device):
                    array = _array
                    break

            mountpoint = getattr(array, "name", "")
        else:
            mountpoint = getattr(format, "mountpoint", "")
            if mountpoint is None:
                mountpoint = ""

        # device name
        name = getattr(device, "lvname", device.name)

        # label
        label = getattr(format, "label", "")
        if label is None:
            label = ""

        item.setDevice(device)
        item.setName(name)
        item.setMountpoint(mountpoint)
        item.setLabel(label)
        item.setType(format.name)
        item.setSize("%Ld" % device.size)
        item.setFormat(formatIcon)
        item.setExpanded(True)

    def populate(self):
        # Clear device tree
        self.ui.deviceTree.clear()

        # first do LVM
        vgs = self.storage.vgs
        if vgs:
            volumeGroupsItem = DeviceTreeItem(self.ui.deviceTree)
            volumeGroupsItem.setName(_("Volume Groups"))
            volumeGroupsItem.setExpanded(True)
            for vg in vgs:
                volumeGroupItem = DeviceTreeItem(volumeGroupsItem)
                self.addDevice(vg, volumeGroupItem)
                volumeGroupItem.setType("")
                for lv in vg.lvs:
                    logicalVolumeItem = DeviceTreeItem(volumeGroupItem)
                    self.addDevice(lv, logicalVolumeItem)

                # We add a row for the VG free space.
                if vg.freeSpace > 0:
                    freeLogicalVolumeItem = DeviceTreeItem(volumeGroupItem)
                    freeLogicalVolumeItem.setName(_("Free"))
                    freeLogicalVolumeItem.setSize("%Ld" % vg.freeSpace)
                    freeLogicalVolumeItem.setDevice(None)
                    freeLogicalVolumeItem.setMountpoint("")

        # handle RAID next
        raidarrays = self.storage.raidArrays
        if raidarrays:
            raidArraysItem = DeviceTreeItem(self.ui.deviceTree)
            raidArraysItem.setName(_("Raid Arrays"))
            raidArraysItem.setExpanded(True)
            for array in raidarrays:
                raidArrayItem = DeviceTreeItem(raidArraysItem)
                self.addDevice(array, raidArrayItem)

        # now normal partitions
        disks = self.storage.partitioned
        # also include unpartitioned disks that aren't mpath or biosraid
        whole = filter(lambda d: not d.partitioned and not d.format.hidden,
                       self.storage.disks)
        disks.extend(whole)
        disks.sort(key=lambda d: d.name)
        # Disk&Partitions
        drivesItem = DeviceTreeItem(self.ui.deviceTree)
        drivesItem.setName(_("Hard Drives"))
        drivesItem.setExpanded(True)
        for disk in disks:
            diskItem = DeviceTreeItem(drivesItem)
            diskItem.setExpanded(True)
            diskItem.setName("%s - %s" % (disk.model, disk.name))
            #self.ui.deviceTree.expandItem(diskItem)
            if disk.partitioned:
                partition = disk.format.firstPartition
                extendedItem = None
                while partition:
                    if partition.type & parted.PARTITION_METADATA:
                        partition = partition.nextPartition()
                        continue

                    partName = devicePathToName(partition.getDeviceNodeName())
                    device = self.storage.devicetree.getDeviceByName(partName)

                    if not device and not partition.type & parted.PARTITION_FREESPACE:
                        ctx.logger.debug(
                            "can't find partition %s in device tree" %
                            partName)

                    # Force partitions tree item not to be less than 12 MB
                    if partition.getSize(unit="MB") <= 12.0:
                        if not partition.active or not partition.getFlag(
                                parted.PARTITION_BOOT):
                            partition = partition.nextPartition()
                            continue

                    if device and device.isExtended:
                        if extendedItem:
                            raise RuntimeError, _(
                                "Can't handle more than "
                                "one extended partition per disk")
                        extendedItem = partItem = DeviceTreeItem(diskItem)
                        partitionItem = extendedItem

                    elif device and device.isLogical:
                        if not extendedItem:
                            raise RuntimeError, _(
                                "Crossed logical partition before extended")
                        partitionItem = DeviceTreeItem(extendedItem)

                    else:
                        # Free space item
                        if partition.type & parted.PARTITION_LOGICAL:
                            partitionItem = DeviceTreeItem(extendedItem)
                        else:
                            partitionItem = DeviceTreeItem(diskItem)

                    if device and not device.isExtended:
                        self.addDevice(device, partitionItem)
                    else:
                        # either extended or freespace
                        if partition.type & parted.PARTITION_FREESPACE:
                            deviceName = _("Free")
                            device = partition
                            deviceType = ""
                        else:
                            deviceName = device.name
                            deviceType = _("Extended")

                        partitionItem.setName(deviceName)
                        partitionItem.setType(deviceType)
                        size = partition.getSize(unit="MB")
                        if size < 1.0:
                            size = "< 1"
                        else:
                            size = "%Ld" % (size)
                        partitionItem.setSize(size)
                        partitionItem.setDevice(device)

                    partition = partition.nextPartition()
            else:
                self.addDevice(disk, diskItem)

    def refresh(self, justRedraw=None):
        ctx.logger.debug("refresh: justRedraw=%s" % justRedraw)
        self.ui.deviceTree.clear()
        if justRedraw:
            rc = 0
        else:
            try:
                doPartitioning(self.storage)
                rc = 0
            except PartitioningError, msg:
                self.intf.messageWindow(
                    _("Error Partitioning"),
                    _("Could not allocate requested partitions: %s.") % msg,
                    customIcon="error")
                rc = -1
            except PartitioningWarning, msg:
                rc = self.intf.messageWindow(
                    _("Warning Partitioning"),
                    _("Warning: %s.") % msg,
                    customButtons=[_("Modify Partition"),
                                   _("Continue")],
                    customIcon="warning")
                if rc == 1:
                    rc = -1
                else:
                    rc = 0
                    all_devices = self.storage.devicetree.devices
                    bootDevs = [d for d in all_devices if d.bootable]
예제 #24
0
파일: Manager.py 프로젝트: razman786/tigger
class ImageManager(QWidget):
    """An ImageManager manages a stack of images (and associated ImageControllers)"""
    showErrorMessage = pyqtSignal(str, int)
    imagesChanged = pyqtSignal()
    imageRaised = pyqtSignal(FITSImagePlotItem)
    imagePlotRaised = pyqtSignal()

    def __init__(self, *args):
        QWidget.__init__(self, *args)
        self.mainwin = None
        # init layout
        self._lo = QVBoxLayout(self)
        self._lo.setContentsMargins(0, 0, 0, 0)
        self._lo.setSpacing(0)
        # init internal state
        self._currier = PersistentCurrier()
        self._z0 = 0  # z-depth of first image, the rest count down from it
        self._updating_imap = False
        self._locked_display_range = False
        self._imagecons = []
        self._imagecon_loadorder = []
        self._center_image = None
        self._plot = None
        self._border_pen = None
        self._drawing_key = None
        self._load_image_dialog = None
        self._label_color = None
        self._label_bg_brush = None
        self._model_imagecons = set()
        # init menu and standard actions
        self._menu = QMenu("&Image", self)
        qag = QActionGroup(self)
        # exclusive controls for plotting topmost or all images
        self._qa_plot_top = qag.addAction("Display topmost image only")
        self._qa_plot_all = qag.addAction("Display all images")
        self._qa_plot_top.setCheckable(True)
        self._qa_plot_all.setCheckable(True)
        self._qa_plot_top.setChecked(True)
        self._qa_plot_all.toggled[bool].connect(self._displayAllImages)
        self._closing = False

        self._qa_load_clipboard = None
        self._clipboard_mode = QClipboard.Clipboard
        QApplication.clipboard().changed[QClipboard.Mode].connect(
            self._checkClipboardPath)
        # populate the menu
        self._repopulateMenu()
        self.signalShowMessage = None
        self.signalShowErrorMessage = None

    def close(self):
        dprint(1, "closing Manager")
        self._closing = True
        for ic in self._imagecons:
            ic.close()

    def setShowMessageSignal(self, _signal):
        self.signalShowMessage = _signal

    def setShowErrorMessageSignal(self, _signal):
        self.signalShowErrorMessage = _signal

    def setMainWindow(self, _mainwin):
        self.mainwin = _mainwin

    def loadImage(self,
                  filename=None,
                  duplicate=True,
                  to_top=True,
                  model=None):
        """Loads image. Returns ImageControlBar object.
        If image is already loaded: returns old ICB if duplicate=False (raises to top if to_top=True),
        or else makes a new control bar.
        If model is set to a source name, marks the image as associated with a model source. These can be unloaded en masse by calling
        unloadModelImages().
        """
        if filename is None:
            if not self._load_image_dialog:
                dialog = self._load_image_dialog = QFileDialog(
                    self, "Load FITS image", ".",
                    "FITS images (%s);;All files (*)" %
                    (" ".join(["*" + ext for ext in FITS_ExtensionList])))
                dialog.setFileMode(QFileDialog.ExistingFile)
                dialog.setModal(True)
                dialog.filesSelected['QStringList'].connect(self.loadImage)
            self._load_image_dialog.exec_()
            return None
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # report error if image does not exist
        if not os.path.exists(filename):
            self.signalShowErrorMessage.emit(
                """FITS image %s does not exist.""" % filename)
            return None
        # see if image is already loaded
        if not duplicate:
            for ic in self._imagecons:
                if ic.getFilename() and os.path.samefile(
                        filename, ic.getFilename()):
                    if to_top:
                        self.raiseImage(ic)
                    if model:
                        self._model_imagecons.add(id(ic))
                    return ic
        # load the FITS image
        busy = BusyIndicator()
        dprint(2, "reading FITS image", filename)
        self.signalShowMessage.emit("""Reading FITS image %s""" % filename,
                                    3000)
        QApplication.flush()
        try:
            image = SkyImage.FITSImagePlotItem(str(filename))
        except KeyboardInterrupt:
            raise
        except:
            busy.reset_cursor()
            traceback.print_exc()
            print(
                """Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer,
          please send the FITS file, along with a copy of any error messages from the text console, to [email protected]."""
                % (filename, str(sys.exc_info()[1])))
            self.signalShowErrorMessage.emit(
                """<P>Error loading FITS image %s: %s. This may be due to a bug in Tigger; if the FITS file loads fine in another viewer,
          please send the FITS file, along with a copy of any error messages from the text console, to [email protected].</P>"""
                % (filename, str(sys.exc_info()[1])))
            return None
        # create control bar, add to widget stack
        ic = self._createImageController(image,
                                         "model source '%s'" %
                                         model if model else filename,
                                         model or image.name,
                                         model=model)
        print("""Loaded FITS image %s""" % filename)
        self.signalShowMessage.emit("""Loaded FITS image %s""" % filename,
                                    3000)
        busy.reset_cursor()
        return ic

    def setZ0(self, z0):
        self._z0 = z0
        if self._imagecons:
            self.raiseImage(self._imagecons[0])

    def enableImageBorders(self, border_pen, label_color, label_bg_brush):
        self._border_pen, self._label_color, self._label_bg_brush = \
            border_pen, label_color, label_bg_brush

    def lockAllDisplayRanges(self, rc0, curry=False):
        """Locks all display ranges, and sets the intensity from rc0"""
        if not self._updating_imap:
            self._updating_imap = True
            rc0.lockDisplayRange()
            try:
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc0:
                        rc1.setDisplayRange(*rc0.displayRange())
                        rc1.lockDisplayRange()
            finally:
                self._updating_imap = False

    def unlockAllDisplayRanges(self):
        """Unlocks all display range."""
        for ic in self._imagecons:
            ic.renderControl().lockDisplayRange(False)

    def _lockDisplayRange(self, rc0, lock):
        """Locks or unlocks the display range of a specific controller."""
        if lock and not self._updating_imap:
            self._updating_imap = True
            try:
                # if something is already locked, copy display range from it
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc0 and rc1.isDisplayRangeLocked():
                        rc0.setDisplayRange(*rc1.displayRange())
            finally:
                self._updating_imap = False

    def _updateDisplayRange(self, rc, dmin, dmax):
        """This is called whenever one of the images (or rather, its associated RenderControl object) changes its display range."""
        if not rc.isDisplayRangeLocked():
            return
        # If the display range is locked, propagate it to all images.
        # but don't do it if we're already propagating (otherwise we may get called in an infinte loop)
        if not self._updating_imap:
            self._updating_imap = True
            try:
                for ic in self._imagecons:
                    rc1 = ic.renderControl()
                    if rc1 is not rc and rc1.isDisplayRangeLocked():
                        rc1.setDisplayRange(dmin, dmax)
            finally:
                self._updating_imap = False

    def getImages(self):
        return [ic.image for ic in self._imagecons]

    def getTopImage(self):
        return (self._imagecons or None) and self._imagecons[0].image

    def cycleImages(self):
        index = self._imagecon_loadorder.index(self._imagecons[0])
        index = (index + 1) % len(self._imagecon_loadorder)
        self.raiseImage(self._imagecon_loadorder[index])

    def blinkImages(self):
        if len(self._imagecons) > 1:
            self.raiseImage(self._imagecons[1])

    def incrementSlice(self, extra_axis, incr):
        if self._imagecons:
            rc = self._imagecons[0].renderControl()
            sliced_axes = rc.slicedAxes()
            if extra_axis < len(sliced_axes):
                rc.incrementSlice(sliced_axes[extra_axis][0], incr)

    def setLMRectSubset(self, rect):
        if self._imagecons:
            self._imagecons[0].setLMRectSubset(rect)

    def getLMRectStats(self, rect):
        if self._imagecons:
            return self._imagecons[0].renderControl().getLMRectStats(rect)

    def unloadModelImages(self):
        """Unloads images associated with model (i.e. loaded with the model=True flag)"""
        for ic in [
                ic for ic in self._imagecons if id(ic) in self._model_imagecons
        ]:
            self.unloadImage(ic)

    def unloadImage(self, imagecon, foo=None):
        """Unloads the given imagecon object."""
        if imagecon not in self._imagecons:
            return
        # recenter if needed
        self._imagecons.remove(imagecon)
        self._imagecon_loadorder.remove(imagecon)
        self._model_imagecons.discard(id(imagecon))
        # remove dockable widget
        imagecon.removeDockWidget()
        # reparent widget and release it
        imagecon.setParent(None)
        imagecon.close()
        # recenter image, if unloaded the center image
        if self._center_image is imagecon.image:
            self.centerImage(self._imagecons[0] if self._imagecons else None,
                             emit=False)
        # emit signal
        self._repopulateMenu()
        self.imagesChanged.emit()
        if self._imagecons:
            self.raiseImage(self._imagecons[0])
        else:
            # remove all dock widgets
            widget_list = self.mainwin.findChildren(QDockWidget)
            for widget in widget_list:
                self.mainwin.removeDockWidget(widget)
                widget.bind_widget.setVisible(False)
                widget.close()
            if self.mainwin._current_layout is not self.mainwin.LayoutImageModel:
                self.mainwin.skyplot.setVisible(False)
            # reset size to be minus dockables - workaround for bug #164
            # self.mainwin.setMaximumWidth(self.mainwin.width() - 700)

    def getCenterImage(self):
        return self._center_image

    def centerImage(self, imagecon, emit=True):
        self._center_image = imagecon and imagecon.image
        for ic in self._imagecons:
            ic.setPlotProjection(self._center_image.projection)
        if emit or emit is None:  # added this check as curry() call to this method via signal can be emit=None.
            self.imagesChanged.emit()

    def raiseImage(self, imagecon, foo=None):
        busy = None
        # reshuffle image stack, if more than one image image
        if len(self._imagecons) > 1:
            busy = BusyIndicator()
            # reshuffle image stack
            self._imagecons.remove(imagecon)
            self._imagecons.insert(0, imagecon)
            # notify imagecons
            for i, ic in enumerate(self._imagecons):
                label = "%d" % (i + 1) if i else "<B>1</B>"
                ic.setZ(self._z0 - i * 10,
                        top=not i,
                        depthlabel=label,
                        can_raise=True)
            # adjust visibility
            for j, ic in enumerate(self._imagecons):
                ic.setImageVisible(not j
                                   or bool(self._qa_plot_all.isChecked()))
            # issue replot signal fixed with assumption that this signal is now correct according to the old version
            # self.imageRaised.emit(self._imagecons[0])  # This was the old signal
            self.imagePlotRaised.emit()
            self.fastReplot()
        # else simply update labels
        else:
            self._imagecons[0].setZ(self._z0,
                                    top=True,
                                    depthlabel=None,
                                    can_raise=False)
            self._imagecons[0].setImageVisible(True)
        # update slice menus
        img = imagecon.image
        axes = imagecon.renderControl().slicedAxes()
        for i, (_next, _prev) in enumerate(self._qa_slices):
            _next.setVisible(False)
            _prev.setVisible(False)
            if i < len(axes):
                iaxis, name, labels = axes[i]
                _next.setVisible(True)
                _prev.setVisible(True)
                _next.setText("Show next slice along %s axis" % name)
                _prev.setText("Show previous slice along %s axis" % name)
        # emit signals
        self.imageRaised.emit(img)
        # if dockable control dialog is docked and tabbed then raise to front
        if imagecon._dockable_colour_ctrl is not None:
            if imagecon._dockable_colour_ctrl.isVisible():
                if not imagecon._dockable_colour_ctrl.isFloating():
                    list_of_tabbed_widgets = self.mainwin.tabifiedDockWidgets(
                        imagecon._dockable_colour_ctrl)
                    if list_of_tabbed_widgets:
                        imagecon._dockable_colour_ctrl.raise_()
        if busy is not None:
            busy.reset_cursor()

    def resetDrawKey(self):
        """Makes and sets the current plot's drawing key"""
        if self._plot:
            key = []
            for ic in self._imagecons:
                key.append(id(ic))
                key += ic.currentSlice()
                self._plot.setDrawingKey(tuple(key))

    def fastReplot(self, *dum):
        """Fast replot -- called when flipping images or slices. Uses the plot cache, if possible."""
        if self._plot:
            self.resetDrawKey()
            dprint(2, "calling replot", time.time() % 60)
            self._plot.replot()
            dprint(2, "replot done", time.time() % 60)

    def replot(self, *dum):
        """Proper replot -- called when an image needs to be properly redrawn. Cleares the plot's drawing cache."""
        if self._plot:
            self._plot.clearDrawCache()
            self.resetDrawKey()
            self._plot.replot()

    def attachImagesToPlot(self, plot):
        self._plot = plot
        self.resetDrawKey()
        for ic in self._imagecons:
            ic.attachToPlot(plot)

    def getMenu(self):
        return self._menu

    def _displayAllImages(self, enabled):
        busy = BusyIndicator()
        if enabled:
            for ic in self._imagecons:
                ic.setImageVisible(True)
        else:
            self._imagecons[0].setImageVisible(True)
            for ic in self._imagecons[1:]:
                ic.setImageVisible(False)
        self.replot()
        busy.reset_cursor()

    def _checkClipboardPath(self, mode=QClipboard.Clipboard):
        if self._qa_load_clipboard:
            self._clipboard_mode = mode
            try:
                path = str(QApplication.clipboard().text(mode))
            except:
                path = None
            self._qa_load_clipboard.setEnabled(
                bool(path and os.path.isfile(path)))

    def _loadClipboardPath(self):
        try:
            path = QApplication.clipboard().text(self._clipboard_mode)
        except:
            return
        self.loadImage(path)

    def _repopulateMenu(self):
        self._menu.clear()
        self._menu.addAction("&Load image...", self.loadImage,
                             Qt.CTRL + Qt.Key_L)
        self._menu.addAction("&Compute image...", self.computeImage,
                             Qt.CTRL + Qt.Key_M)
        self._qa_load_clipboard = self._menu.addAction(
            "Load from clipboard &path", self._loadClipboardPath,
            Qt.CTRL + Qt.Key_P)
        self._checkClipboardPath()
        if self._imagecons:
            self._menu.addSeparator()
            # add controls to cycle images and planes
            for i, imgcon in enumerate(self._imagecons[::-1]):
                self._menu.addMenu(imgcon.getMenu())
            self._menu.addSeparator()
            if len(self._imagecons) > 1:
                self._menu.addAction("Cycle images", self.cycleImages,
                                     Qt.Key_F5)
                self._menu.addAction("Blink images", self.blinkImages,
                                     Qt.Key_F6)
            self._qa_slices = ((self._menu.addAction(
                "Next slice along axis 1",
                self._currier.curry(self.incrementSlice, 0, 1), Qt.Key_F7),
                                self._menu.addAction(
                                    "Previous slice along axis 1",
                                    self._currier.curry(
                                        self.incrementSlice, 0, -1),
                                    Qt.SHIFT + Qt.Key_F7)),
                               (self._menu.addAction(
                                   "Next slice along axis 2",
                                   self._currier.curry(self.incrementSlice, 1,
                                                       1), Qt.Key_F8),
                                self._menu.addAction(
                                    "Previous slice along axis 2",
                                    self._currier.curry(
                                        self.incrementSlice, 1, -1),
                                    Qt.SHIFT + Qt.Key_F8)))
            self._menu.addSeparator()
            self._menu.addAction(self._qa_plot_top)
            self._menu.addAction(self._qa_plot_all)

    def computeImage(self, expression=None):
        """Computes image from expression (if expression is None, pops up dialog)"""
        if expression is None:
            (expression, ok) = QInputDialog.getText(
                self, "Compute image", """Enter an image expression to compute.
                                              Any valid numpy expression is supported, and
                                              all functions from the numpy module are available (including sub-modules such as fft).
                                              Use 'a', 'b', 'c' to refer to images.
                                              Examples:  "(a+b)/2", "cos(a)+sin(b)", "a-a.mean()", "fft.fft2(a)", etc."""
            )
            #      (expression,ok) = QInputDialog.getText(self,"Compute image","""<P>Enter an expression to compute.
            #        Use 'a', 'b', etc. to refer to loaded images. Any valid numpy expression is supported, and all the
            #       functions from the numpy module are available. Examples of valid expressions include "(a+b)/2",
            #       "cos(a)+sin(b)", "a-a.mean()", etc.
            #        </P>
            #      """)
            expression = str(expression)
            if not ok or not expression:
                return
        # try to parse expression
        arglist = [(chr(ord('a') + ic.getNumber()), ic.image)
                   for ic in self._imagecons]
        try:
            exprfunc = eval(
                "lambda " + (",".join([x[0]
                                       for x in arglist])) + ":" + expression,
                numpy.__dict__, {})
        except Exception as exc:
            self.signalShowErrorMessage.emit(
                """Error parsing expression "%s": %s.""" %
                (expression, str(exc)))
            return None
        # try to evaluate expression
        self.signalShowMessage.emit("Computing expression \"%s\"" % expression,
                                    10000)
        busy = BusyIndicator()
        QApplication.flush()

        # trim trivial trailing dimensions. This avoids the problem of when an NxMx1 and an NxMx1x1 arrays are added,
        # the result is promoted to NxMxMx1 following the numpy rules.
        def trimshape(shape):
            out = shape
            while out and out[-1] == 1:
                out = out[:-1]
            return out

        def trimarray(array):
            return array.reshape(trimshape(array.shape))

        try:
            result = exprfunc(*[trimarray(x[1].data()) for x in arglist])
        except Exception as exc:
            busy.reset_cursor()
            traceback.print_exc()
            self.signalShowErrorMessage.emit("""Error evaluating "%s": %s.""" %
                                             (expression, str(exc)))
            return None
        busy.reset_cursor()
        if type(result) != numpy.ma.masked_array and type(
                result) != numpy.ndarray:
            self.signalShowErrorMessage.emit(
                """Result of "%s" is of invalid type "%s" (array expected)."""
                % (expression, type(result).__name__))
            return None
        # convert coomplex results to real
        if numpy.iscomplexobj(result):
            self.signalShowErrorMessage.emit(
                """Result of "%s" is complex. Complex images are currently
      not fully supported, so we'll implicitly use the absolute value instead."""
                % (expression))
            expression = "abs(%s)" % expression
            result = abs(result)
        # determine which image this expression can be associated with
        res_shape = trimshape(result.shape)
        arglist = [
            x for x in arglist if hasattr(x[1], 'fits_header')
            and trimshape(x[1].data().shape) == res_shape
        ]
        if not arglist:
            self.signalShowErrorMessage.emit(
                """Result of "%s" has shape %s, which does not match any loaded FITS image."""
                % (expression, "x".join(map(str, result.shape))))
            return None
        # look for an image in the arglist with the same projection, and with a valid dirname
        # (for the where-to-save hint)
        template = arglist[0][1]
        # if all images in arglist have the same projection, then it doesn't matter what we use
        # else ask
        if len(
            [x for x in arglist[1:] if x[1].projection == template.projection
             ]) != len(arglist) - 1:
            options = [x[0] for x in arglist]
            (which, ok) = QInputDialog.getItem(
                self, "Compute image",
                "Coordinate system to use for the result of \"%s\":" %
                expression, options, 0, False)
            if not ok:
                return None
            try:
                template = arglist[options.index(which)][1]
            except:
                pass
        # create a FITS image
        busy = BusyIndicator()
        dprint(2, "creating FITS image", expression)
        self.signalShowMessage.emit("""Creating image for %s""" % expression,
                                    3000)
        QApplication.flush()
        try:
            hdu = pyfits.PrimaryHDU(result.transpose(), template.fits_header)
            skyimage = SkyImage.FITSImagePlotItem(name=expression,
                                                  filename=None,
                                                  hdu=hdu)
        except:
            busy.reset_cursor()
            traceback.print_exc()
            self.signalShowErrorMessage.emit(
                """Error creating FITS image %s: %s""" %
                (expression, str(sys.exc_info()[1])))
            return None
        # get directory name for save-to hint
        dirname = getattr(template, 'filename', None)
        if not dirname:
            dirnames = [
                getattr(img, 'filename') for x, img in arglist
                if hasattr(img, 'filename')
            ]
            dirname = dirnames[0] if dirnames else None
        # create control bar, add to widget stack
        self._createImageController(
            skyimage,
            expression,
            expression,
            save=((dirname and os.path.dirname(dirname)) or "."))
        self.signalShowMessage.emit("Created new image for %s" % expression,
                                    3000)
        dprint(2, "image created")
        busy.reset_cursor()

    def _createImageController(self,
                               image,
                               name,
                               basename,
                               model=False,
                               save=False):
        dprint(2, "creating ImageController for", name)
        ic = ImageController(image, self, self, name, save=save)
        # attach appropriate signals
        ic.imageSignalRepaint.connect(self.replot)
        ic.imageSignalSlice.connect(self.fastReplot)
        image.connectPlotRiased(self.imagePlotRaised)
        ic.imageSignalRaise.connect(self._currier.curry(self.raiseImage, ic))
        ic.imageSignalUnload.connect(self._currier.curry(self.unloadImage, ic))
        ic.imageSignalCenter.connect(self._currier.curry(self.centerImage, ic))
        ic.setNumber(len(self._imagecons))
        self._imagecons.insert(0, ic)
        self._imagecon_loadorder.append(ic)
        if model:
            self._model_imagecons.add(id(ic))
        self._lo.addWidget(ic)
        if self._border_pen:
            ic.addPlotBorder(self._border_pen, basename, self._label_color,
                             self._label_bg_brush)
        ic.renderControl().displayRangeChanged.connect(
            self._currier.curry(self._updateDisplayRange, ic.renderControl()))
        ic.renderControl().displayRangeLocked.connect(
            self._currier.curry(self._lockDisplayRange, ic.renderControl()))
        self._plot = None
        # add to menus
        dprint(2, "repopulating menus")
        self._repopulateMenu()
        # center and raise to top of stack
        self.raiseImage(ic)
        if not self._center_image:
            self.centerImage(ic, emit=False)
        else:
            ic.setPlotProjection(self._center_image.projection)
        # signal
        self.imagesChanged.emit()
        return ic
예제 #25
0
class TagsView(QTreeView):  # {{{

    refresh_required        = pyqtSignal()
    tags_marked             = pyqtSignal(object)
    edit_user_category      = pyqtSignal(object)
    delete_user_category    = pyqtSignal(object)
    del_item_from_user_cat  = pyqtSignal(object, object, object)
    add_item_to_user_cat    = pyqtSignal(object, object, object)
    add_subcategory         = pyqtSignal(object)
    tags_list_edit          = pyqtSignal(object, object, object)
    saved_search_edit       = pyqtSignal(object)
    rebuild_saved_searches  = pyqtSignal()
    author_sort_edit        = pyqtSignal(object, object, object, object, object)
    tag_item_renamed        = pyqtSignal()
    search_item_renamed     = pyqtSignal()
    drag_drop_finished      = pyqtSignal(object)
    restriction_error       = pyqtSignal()
    tag_item_delete         = pyqtSignal(object, object, object, object, object)
    apply_tag_to_selected   = pyqtSignal(object, object, object)

    def __init__(self, parent=None):
        QTreeView.__init__(self, parent=None)
        self.setMouseTracking(True)
        self.alter_tb = None
        self.disable_recounting = False
        self.setUniformRowHeights(True)
        self.setIconSize(QSize(20, 20))
        self.setTabKeyNavigation(True)
        self.setAnimated(True)
        self.setHeaderHidden(True)
        self.setItemDelegate(TagDelegate(self))
        self.made_connections = False
        self.setAcceptDrops(True)
        self.setDragEnabled(True)
        self.setDragDropMode(self.DragDrop)
        self.setDropIndicatorShown(True)
        self.in_drag_drop = False
        self.setAutoExpandDelay(500)
        self.pane_is_visible = False
        self.search_icon = QIcon(I('search.png'))
        self.search_copy_icon = QIcon(I("search_copy_saved.png"))
        self.user_category_icon = QIcon(I('tb_folder.png'))
        self.edit_metadata_icon = QIcon(I('edit_input.png'))
        self.delete_icon = QIcon(I('list_remove.png'))
        self.rename_icon = QIcon(I('edit-undo.png'))

        self._model = TagsModel(self)
        self._model.search_item_renamed.connect(self.search_item_renamed)
        self._model.refresh_required.connect(self.refresh_required,
                type=Qt.QueuedConnection)
        self._model.tag_item_renamed.connect(self.tag_item_renamed)
        self._model.restriction_error.connect(self.restriction_error)
        self._model.user_categories_edited.connect(self.user_categories_edited,
                type=Qt.QueuedConnection)
        self._model.drag_drop_finished.connect(self.drag_drop_finished)
        self.set_look_and_feel()
        # Allowing keyboard focus looks bad in the Qt Fusion style and is useless
        # anyway since the enter/spacebar keys do nothing
        self.setFocusPolicy(Qt.NoFocus)
        QApplication.instance().palette_changed.connect(self.set_style_sheet, type=Qt.QueuedConnection)

    def set_style_sheet(self):
        stylish_tb = '''
                QTreeView {
                    background-color: palette(window);
                    color: palette(window-text);
                    border: none;
                }
        '''
        self.setStyleSheet('''
                QTreeView::item {
                    border: 1px solid transparent;
                    padding-top:PADex;
                    padding-bottom:PADex;
                }

                QTreeView::item:hover {
                    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
                    border: 1px solid #bfcde4;
                    border-radius: 6px;
                }
        '''.replace('PAD', unicode_type(gprefs['tag_browser_item_padding'])) + (
            '' if gprefs['tag_browser_old_look'] else stylish_tb))

    def set_look_and_feel(self):
        self.set_style_sheet()
        self.setAlternatingRowColors(gprefs['tag_browser_old_look'])
        self.itemDelegate().old_look = gprefs['tag_browser_old_look']

    @property
    def hidden_categories(self):
        return self._model.hidden_categories

    @property
    def db(self):
        return self._model.db

    @property
    def collapse_model(self):
        return self._model.collapse_model

    def set_pane_is_visible(self, to_what):
        pv = self.pane_is_visible
        self.pane_is_visible = to_what
        if to_what and not pv:
            self.recount()

    def get_state(self):
        state_map = {}
        expanded_categories = []
        hide_empty_categories = self.model().prefs['tag_browser_hide_empty_categories']
        crmap = self._model.category_row_map()
        for category in self._model.category_nodes:
            if (category.category_key in self.hidden_categories or (
                hide_empty_categories and len(category.child_tags()) == 0)):
                continue
            row = crmap.get(category.category_key)
            if row is not None:
                index = self._model.index(row, 0, QModelIndex())
                if self.isExpanded(index):
                    expanded_categories.append(category.category_key)
            states = [c.tag.state for c in category.child_tags()]
            names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
            state_map[category.category_key] = dict(zip(names, states))
        return expanded_categories, state_map

    def reread_collapse_parameters(self):
        self._model.reread_collapse_model(self.get_state()[1])

    def set_database(self, db, alter_tb):
        self._model.set_database(db)
        self.alter_tb = alter_tb
        self.pane_is_visible = True  # because TagsModel.set_database did a recount
        self.setModel(self._model)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        pop = self.db.CATEGORY_SORTS.index(config['sort_tags_by'])
        self.alter_tb.sort_menu.actions()[pop].setChecked(True)
        try:
            match_pop = self.db.MATCH_TYPE.index(config['match_tags_type'])
        except ValueError:
            match_pop = 0
        self.alter_tb.match_menu.actions()[match_pop].setChecked(True)
        if not self.made_connections:
            self.clicked.connect(self.toggle)
            self.customContextMenuRequested.connect(self.show_context_menu)
            self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
            self.alter_tb.sort_menu.triggered.connect(self.sort_changed)
            self.alter_tb.match_menu.triggered.connect(self.match_changed)
            self.made_connections = True
        self.refresh_signal_processed = True
        db.add_listener(self.database_changed)
        self.expanded.connect(self.item_expanded)

    def database_changed(self, event, ids):
        if self.refresh_signal_processed:
            self.refresh_signal_processed = False
            self.refresh_required.emit()

    def user_categories_edited(self, user_cats, nkey):
        state_map = self.get_state()[1]
        self.db.new_api.set_pref('user_categories', user_cats)
        self._model.rebuild_node_tree(state_map=state_map)
        p = self._model.find_category_node('@'+nkey)
        self.show_item_at_path(p)

    @property
    def match_all(self):
        return (self.alter_tb and self.alter_tb.match_menu.actions()[1].isChecked())

    def sort_changed(self, action):
        for i, ac in enumerate(self.alter_tb.sort_menu.actions()):
            if ac is action:
                config.set('sort_tags_by', self.db.CATEGORY_SORTS[i])
                self.recount()
                break

    def match_changed(self, action):
        try:
            for i, ac in enumerate(self.alter_tb.match_menu.actions()):
                if ac is action:
                    config.set('match_tags_type', self.db.MATCH_TYPE[i])
        except:
            pass

    def mousePressEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.possible_drag_start = event.pos()
        return QTreeView.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        dex = self.indexAt(event.pos())
        if dex.isValid():
            self.setCursor(Qt.PointingHandCursor)
        else:
            self.unsetCursor()
        if not event.buttons() & Qt.LeftButton:
            return
        if self.in_drag_drop or not dex.isValid():
            QTreeView.mouseMoveEvent(self, event)
            return
        # don't start drag/drop until the mouse has moved a bit.
        if ((event.pos() - self.possible_drag_start).manhattanLength() <
                                    QApplication.startDragDistance()):
            QTreeView.mouseMoveEvent(self, event)
            return
        # Must deal with odd case where the node being dragged is 'virtual',
        # created to form a hierarchy. We can't really drag this node, but in
        # addition we can't allow drag recognition to notice going over some
        # other node and grabbing that one. So we set in_drag_drop to prevent
        # this from happening, turning it off when the user lifts the button.
        self.in_drag_drop = True
        if not self._model.flags(dex) & Qt.ItemIsDragEnabled:
            QTreeView.mouseMoveEvent(self, event)
            return
        md = self._model.mimeData([dex])
        pixmap = dex.data(DRAG_IMAGE_ROLE).pixmap(self.iconSize())
        drag = QDrag(self)
        drag.setPixmap(pixmap)
        drag.setMimeData(md)
        if (self._model.is_in_user_category(dex) or
                    self._model.is_index_on_a_hierarchical_category(dex)):
            '''
            Things break if we specify MoveAction as the default, which is
            what we want for drag on hierarchical categories. Dragging user
            categories stops working. Don't know why. To avoid the problem
            we fix the action in dragMoveEvent.
            '''
            drag.exec_(Qt.CopyAction|Qt.MoveAction, Qt.CopyAction)
        else:
            drag.exec_(Qt.CopyAction)

    def mouseReleaseEvent(self, event):
        # Swallow everything except leftButton so context menus work correctly
        if event.button() == Qt.LeftButton or self.in_drag_drop:
            QTreeView.mouseReleaseEvent(self, event)
            self.in_drag_drop = False

    def mouseDoubleClickEvent(self, event):
        # swallow these to avoid toggling and editing at the same time
        pass

    @property
    def search_string(self):
        tokens = self._model.tokens()
        joiner = ' and ' if self.match_all else ' or '
        return joiner.join(tokens)

    def toggle_current_index(self):
        ci = self.currentIndex()
        if ci.isValid():
            self.toggle(ci)

    def toggle(self, index):
        self._toggle(index, None)

    def _toggle(self, index, set_to):
        '''
        set_to: if None, advance the state. Otherwise must be one of the values
        in TAG_SEARCH_STATES
        '''
        modifiers = int(QApplication.keyboardModifiers())
        exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
        if self._model.toggle(index, exclusive, set_to=set_to):
            self.tags_marked.emit(self.search_string)

    def conditional_clear(self, search_string):
        if search_string != self.search_string:
            self.clear()

    def context_menu_handler(self, action=None, category=None,
                             key=None, index=None, search_state=None,
                             use_vl=None, is_first_letter=False):
        if not action:
            return
        try:
            if action == 'set_icon':
                try:
                    path = choose_files(self, 'choose_category_icon',
                                _('Change icon for: %s')%key, filters=[
                                ('Images', ['png', 'gif', 'jpg', 'jpeg'])],
                            all_files=False, select_only_single_file=True)
                    if path:
                        path = path[0]
                        p = QIcon(path).pixmap(QSize(128, 128))
                        d = os.path.join(config_dir, 'tb_icons')
                        if not os.path.exists(d):
                            os.makedirs(d)
                        with open(os.path.join(d, 'icon_' + sanitize_file_name(key)+'.png'), 'wb') as f:
                            f.write(pixmap_to_data(p, format='PNG'))
                            path = os.path.basename(f.name)
                        self._model.set_custom_category_icon(key, unicode_type(path))
                        self.recount()
                except:
                    import traceback
                    traceback.print_exc()
                return
            if action == 'clear_icon':
                self._model.set_custom_category_icon(key, None)
                self.recount()
                return

            def set_completion_data(category):
                try:
                    completion_data = self.db.new_api.all_field_names(category)
                except:
                    completion_data = None
                self.itemDelegate().set_completion_data(completion_data)

            if action == 'edit_item_no_vl':
                item = self.model().get_node(index)
                item.use_vl = False
                set_completion_data(category)
                self.edit(index)
                return
            if action == 'edit_item_in_vl':
                item = self.model().get_node(index)
                item.use_vl = True
                set_completion_data(category)
                self.edit(index)
                return
            if action == 'delete_item_in_vl':
                tag = index.tag
                children = index.child_tags()
                self.tag_item_delete.emit(key, tag.id, tag.original_name,
                                          self.model().get_book_ids_to_use(),
                                          children)
                return
            if action == 'delete_item_no_vl':
                tag = index.tag
                children = index.child_tags()
                self.tag_item_delete.emit(key, tag.id, tag.original_name,
                                          None, children)
                return
            if action == 'open_editor':
                self.tags_list_edit.emit(category, key, is_first_letter)
                return
            if action == 'manage_categories':
                self.edit_user_category.emit(category)
                return
            if action == 'search':
                self._toggle(index, set_to=search_state)
                return
            if action == "raw_search":
                from calibre.gui2.ui import get_gui
                get_gui().get_saved_search_text(search_name='search:' + key)
                return
            if action == 'add_to_category':
                tag = index.tag
                if len(index.children) > 0:
                    for c in index.all_children():
                        self.add_item_to_user_cat.emit(category, c.tag.original_name,
                                               c.tag.category)
                self.add_item_to_user_cat.emit(category, tag.original_name,
                                               tag.category)
                return
            if action == 'add_subcategory':
                self.add_subcategory.emit(key)
                return
            if action == 'search_category':
                self._toggle(index, set_to=search_state)
                return
            if action == 'delete_user_category':
                self.delete_user_category.emit(key)
                return
            if action == 'delete_search':
                self.model().db.saved_search_delete(key)
                self.rebuild_saved_searches.emit()
                return
            if action == 'delete_item_from_user_category':
                tag = index.tag
                if len(index.children) > 0:
                    for c in index.children:
                        self.del_item_from_user_cat.emit(key, c.tag.original_name,
                                               c.tag.category)
                self.del_item_from_user_cat.emit(key, tag.original_name, tag.category)
                return
            if action == 'manage_searches':
                self.saved_search_edit.emit(category)
                return
            if action == 'edit_authors':
                self.author_sort_edit.emit(self, index, False, False, is_first_letter)
                return
            if action == 'edit_author_sort':
                self.author_sort_edit.emit(self, index, True, False, is_first_letter)
                return
            if action == 'edit_author_link':
                self.author_sort_edit.emit(self, index, False, True, False)
                return

            reset_filter_categories = True
            if action == 'hide':
                self.hidden_categories.add(category)
            elif action == 'show':
                self.hidden_categories.discard(category)
            elif action == 'categorization':
                changed = self.collapse_model != category
                self._model.collapse_model = category
                if changed:
                    reset_filter_categories = False
                    gprefs['tags_browser_partition_method'] = category
            elif action == 'defaults':
                self.hidden_categories.clear()
            elif action == 'add_tag':
                item = self.model().get_node(index)
                if item is not None:
                    self.apply_to_selected_books(item)
                return
            elif action == 'remove_tag':
                item = self.model().get_node(index)
                if item is not None:
                    self.apply_to_selected_books(item, True)
                return
            self.db.new_api.set_pref('tag_browser_hidden_categories', list(self.hidden_categories))
            if reset_filter_categories:
                self._model.set_categories_filter(None)
            self._model.rebuild_node_tree()
        except Exception:
            import traceback
            traceback.print_exc()
            return

    def apply_to_selected_books(self, item, remove=False):
        if item.type != item.TAG:
            return
        tag = item.tag
        if not tag.category or not tag.original_name:
            return
        self.apply_tag_to_selected.emit(tag.category, tag.original_name, remove)

    def show_context_menu(self, point):
        def display_name(tag):
            ans = tag.name
            if tag.category == 'search':
                n = tag.name
                if len(n) > 45:
                    n = n[:45] + '...'
                ans = "'" + n + "'"
            elif tag.is_hierarchical and not tag.is_editable:
                ans = tag.original_name
            if ans:
                ans = ans.replace('&', '&&')
            return ans

        index = self.indexAt(point)
        self.context_menu = QMenu(self)

        if index.isValid():
            item = index.data(Qt.UserRole)
            tag = None
            tag_item = item

            if item.type == TagTreeItem.TAG:
                tag = item.tag
                while item.type != TagTreeItem.CATEGORY:
                    item = item.parent

            if item.type == TagTreeItem.CATEGORY:
                if not item.category_key.startswith('@'):
                    while item.parent != self._model.root_item:
                        item = item.parent
                category = unicode_type(item.name or '')
                key = item.category_key
                # Verify that we are working with a field that we know something about
                if key not in self.db.field_metadata:
                    return True
                fm = self.db.field_metadata[key]

                # Did the user click on a leaf node?
                if tag:
                    # If the user right-clicked on an editable item, then offer
                    # the possibility of renaming that item.
                    if tag.is_editable or tag.is_hierarchical:
                        # Add the 'rename' items to both interior and leaf nodes
                        if self.model().get_in_vl():
                            self.context_menu.addAction(self.rename_icon,
                                                    _('Rename %s in Virtual library')%display_name(tag),
                                    partial(self.context_menu_handler, action='edit_item_in_vl',
                                            index=index, category=key))
                        self.context_menu.addAction(self.rename_icon,
                                                _('Rename %s')%display_name(tag),
                                partial(self.context_menu_handler, action='edit_item_no_vl',
                                        index=index, category=key))
                    if tag.is_editable:
                        if key in ('tags', 'series', 'publisher') or \
                                self._model.db.field_metadata.is_custom_field(key):
                            if self.model().get_in_vl():
                                self.context_menu.addAction(self.delete_icon,
                                                    _('Delete %s in Virtual library')%display_name(tag),
                                partial(self.context_menu_handler, action='delete_item_in_vl',
                                    key=key, index=tag_item))

                            self.context_menu.addAction(self.delete_icon,
                                                    _('Delete %s')%display_name(tag),
                                partial(self.context_menu_handler, action='delete_item_no_vl',
                                    key=key, index=tag_item))
                        if key == 'authors':
                            self.context_menu.addAction(_('Edit sort for %s')%display_name(tag),
                                    partial(self.context_menu_handler,
                                            action='edit_author_sort', index=tag.id))
                            self.context_menu.addAction(_('Edit link for %s')%display_name(tag),
                                    partial(self.context_menu_handler,
                                            action='edit_author_link', index=tag.id))

                        # is_editable is also overloaded to mean 'can be added
                        # to a User category'
                        m = QMenu(_('Add %s to User category')%display_name(tag), self.context_menu)
                        m.setIcon(self.user_category_icon)
                        added = [False]

                        def add_node_tree(tree_dict, m, path):
                            p = path[:]
                            for k in sorted(tree_dict.keys(), key=sort_key):
                                p.append(k)
                                n = k[1:] if k.startswith('@') else k
                                m.addAction(self.user_category_icon, n,
                                    partial(self.context_menu_handler,
                                            'add_to_category',
                                            category='.'.join(p), index=tag_item))
                                added[0] = True
                                if len(tree_dict[k]):
                                    tm = m.addMenu(self.user_category_icon,
                                                   _('Children of %s')%n)
                                    add_node_tree(tree_dict[k], tm, p)
                                p.pop()
                        add_node_tree(self.model().user_category_node_tree, m, [])
                        if added[0]:
                            self.context_menu.addMenu(m)

                        # is_editable also means the tag can be applied/removed
                        # from selected books
                        if fm['datatype'] != 'rating':
                            m = self.context_menu.addMenu(self.edit_metadata_icon,
                                            _('Apply %s to selected books')%display_name(tag))
                            m.addAction(QIcon(I('plus.png')),
                                _('Add %s to selected books') % display_name(tag),
                                partial(self.context_menu_handler, action='add_tag', index=index))
                            m.addAction(QIcon(I('minus.png')),
                                _('Remove %s from selected books') % display_name(tag),
                                partial(self.context_menu_handler, action='remove_tag', index=index))

                    elif key == 'search' and tag.is_searchable:
                        self.context_menu.addAction(self.rename_icon,
                                                    _('Rename %s')%display_name(tag),
                            partial(self.context_menu_handler, action='edit_item_no_vl',
                                    index=index))
                        self.context_menu.addAction(self.delete_icon,
                                _('Delete search %s')%display_name(tag),
                                partial(self.context_menu_handler,
                                        action='delete_search', key=tag.original_name))
                    if key.startswith('@') and not item.is_gst:
                        self.context_menu.addAction(self.user_category_icon,
                            _('Remove %(item)s from category %(cat)s')%
                            dict(item=display_name(tag), cat=item.py_name),
                            partial(self.context_menu_handler,
                                    action='delete_item_from_user_category',
                                    key=key, index=tag_item))
                    if tag.is_searchable:
                        # Add the search for value items. All leaf nodes are searchable
                        self.context_menu.addAction(self.search_icon,
                                _('Search for %s')%display_name(tag),
                                partial(self.context_menu_handler, action='search',
                                        search_state=TAG_SEARCH_STATES['mark_plus'],
                                        index=index))
                        self.context_menu.addAction(self.search_icon,
                                _('Search for everything but %s')%display_name(tag),
                                partial(self.context_menu_handler, action='search',
                                        search_state=TAG_SEARCH_STATES['mark_minus'],
                                        index=index))
                        self.context_menu.addAction(self.search_copy_icon,
                                _('Search using saved search expression'),
                                partial(self.context_menu_handler, action='raw_search',
                                        key=tag.name))

                    self.context_menu.addSeparator()
                elif key.startswith('@') and not item.is_gst:
                    if item.can_be_edited:
                        self.context_menu.addAction(self.rename_icon,
                            _('Rename %s')%item.py_name,
                            partial(self.context_menu_handler, action='edit_item_no_vl',
                                    index=index))
                    self.context_menu.addAction(self.user_category_icon,
                            _('Add sub-category to %s')%item.py_name,
                            partial(self.context_menu_handler,
                                    action='add_subcategory', key=key))
                    self.context_menu.addAction(self.delete_icon,
                            _('Delete User category %s')%item.py_name,
                            partial(self.context_menu_handler,
                                    action='delete_user_category', key=key))
                    self.context_menu.addSeparator()
                # Hide/Show/Restore categories
                self.context_menu.addAction(_('Hide category %s') % category,
                    partial(self.context_menu_handler, action='hide',
                            category=key))
                if self.hidden_categories:
                    m = self.context_menu.addMenu(_('Show category'))
                    for col in sorted(self.hidden_categories,
                            key=lambda x: sort_key(self.db.field_metadata[x]['name'])):
                        m.addAction(self.db.field_metadata[col]['name'],
                            partial(self.context_menu_handler, action='show', category=col))

                # search by category. Some categories are not searchable, such
                # as search and news
                if item.tag.is_searchable:
                    self.context_menu.addAction(self.search_icon,
                            _('Search for books in category %s')%category,
                            partial(self.context_menu_handler,
                                    action='search_category',
                                    index=self._model.createIndex(item.row(), 0, item),
                                    search_state=TAG_SEARCH_STATES['mark_plus']))
                    self.context_menu.addAction(self.search_icon,
                            _('Search for books not in category %s')%category,
                            partial(self.context_menu_handler,
                                    action='search_category',
                                    index=self._model.createIndex(item.row(), 0, item),
                                    search_state=TAG_SEARCH_STATES['mark_minus']))
                # Offer specific editors for tags/series/publishers/saved searches
                self.context_menu.addSeparator()
                if key in ['tags', 'publisher', 'series'] or (
                        self.db.field_metadata[key]['is_custom'] and self.db.field_metadata[key]['datatype'] != 'composite'):
                    if tag_item.type == TagTreeItem.CATEGORY and tag_item.temporary:
                        self.context_menu.addAction(_('Manage %s')%category,
                            partial(self.context_menu_handler, action='open_editor',
                                    category=tag_item.name,
                                    key=key, is_first_letter=True))
                    else:
                        self.context_menu.addAction(_('Manage %s')%category,
                            partial(self.context_menu_handler, action='open_editor',
                                    category=tag.original_name if tag else None,
                                    key=key))
                elif key == 'authors':
                    if tag_item.type == TagTreeItem.CATEGORY:
                        if tag_item.temporary:
                            self.context_menu.addAction(_('Manage %s')%category,
                                partial(self.context_menu_handler, action='edit_authors',
                                        index=tag_item.name, is_first_letter=True))
                        else:
                            self.context_menu.addAction(_('Manage %s')%category,
                                partial(self.context_menu_handler, action='edit_authors'))
                    else:
                        self.context_menu.addAction(_('Manage %s')%category,
                            partial(self.context_menu_handler, action='edit_authors',
                                    index=tag.id))
                elif key == 'search':
                    self.context_menu.addAction(_('Manage Saved searches'),
                        partial(self.context_menu_handler, action='manage_searches',
                                category=tag.name if tag else None))

                if tag is None:
                    self.context_menu.addSeparator()
                    self.context_menu.addAction(_('Change category icon'),
                            partial(self.context_menu_handler, action='set_icon', key=key))
                    self.context_menu.addAction(_('Restore default icon'),
                            partial(self.context_menu_handler, action='clear_icon', key=key))

                # Always show the User categories editor
                self.context_menu.addSeparator()
                if key.startswith('@') and \
                        key[1:] in self.db.prefs.get('user_categories', {}).keys():
                    self.context_menu.addAction(_('Manage User categories'),
                            partial(self.context_menu_handler, action='manage_categories',
                                    category=key[1:]))
                else:
                    self.context_menu.addAction(_('Manage User categories'),
                            partial(self.context_menu_handler, action='manage_categories',
                                    category=None))

        if self.hidden_categories:
            if not self.context_menu.isEmpty():
                self.context_menu.addSeparator()
            self.context_menu.addAction(_('Show all categories'),
                        partial(self.context_menu_handler, action='defaults'))

        m = self.context_menu.addMenu(_('Change sub-categorization scheme'))
        da = m.addAction(_('Disable'),
            partial(self.context_menu_handler, action='categorization', category='disable'))
        fla = m.addAction(_('By first letter'),
            partial(self.context_menu_handler, action='categorization', category='first letter'))
        pa = m.addAction(_('Partition'),
            partial(self.context_menu_handler, action='categorization', category='partition'))
        if self.collapse_model == 'disable':
            da.setCheckable(True)
            da.setChecked(True)
        elif self.collapse_model == 'first letter':
            fla.setCheckable(True)
            fla.setChecked(True)
        else:
            pa.setCheckable(True)
            pa.setChecked(True)

        if config['sort_tags_by'] != "name":
            fla.setEnabled(False)
            m.hovered.connect(self.collapse_menu_hovered)
            fla.setToolTip(_('First letter is usable only when sorting by name'))
            # Apparently one cannot set a tooltip to empty, so use a star and
            # deal with it in the hover method
            da.setToolTip('*')
            pa.setToolTip('*')

        if index.isValid() and self.model().rowCount(index) > 0:
            self.context_menu.addSeparator()
            self.context_menu.addAction(_('E&xpand all children'), partial(self.expand_node_and_descendants, index))

        self.context_menu.addAction(_('Collapse all levels'), self.collapseAll)

        if not self.context_menu.isEmpty():
            self.context_menu.popup(self.mapToGlobal(point))
        return True

    def expand_node_and_descendants(self, index):
        if not index.isValid():
            return
        self.expand(index)
        for r in range(self.model().rowCount(index)):
            self.expand_node_and_descendants(index.child(r, 0))

    def collapse_menu_hovered(self, action):
        tip = action.toolTip()
        if tip == '*':
            tip = ''
        QToolTip.showText(QCursor.pos(), tip)

    def dragMoveEvent(self, event):
        QTreeView.dragMoveEvent(self, event)
        self.setDropIndicatorShown(False)
        index = self.indexAt(event.pos())
        if not index.isValid():
            return
        src_is_tb = event.mimeData().hasFormat('application/calibre+from_tag_browser')
        item = index.data(Qt.UserRole)
        if item.type == TagTreeItem.ROOT:
            return

        if src_is_tb:
            src = json_loads(bytes(event.mimeData().data('application/calibre+from_tag_browser')))
            if len(src) == 1:
                src_item = self._model.get_node(self._model.index_for_path(src[0][5]))
                if (src_item.type == TagTreeItem.TAG and
                        src_item.tag.category == item.tag.category and
                        not item.temporary and
                        self._model.is_key_a_hierarchical_category(src_item.tag.category)):
                    event.setDropAction(Qt.MoveAction)
                    self.setDropIndicatorShown(True)
                    return
        if item.type == TagTreeItem.TAG and self._model.flags(index) & Qt.ItemIsDropEnabled:
            event.setDropAction(Qt.CopyAction)
            self.setDropIndicatorShown(not src_is_tb)
            return
        if item.type == TagTreeItem.CATEGORY and not item.is_gst:
            fm_dest = self.db.metadata_for_field(item.category_key)
            if fm_dest['kind'] == 'user':
                if src_is_tb:
                    if event.dropAction() == Qt.MoveAction:
                        # src is initialized above
                        for s in src:
                            if s[0] == TagTreeItem.TAG and \
                                    (not s[1].startswith('@') or s[2]):
                                return
                    self.setDropIndicatorShown(True)
                    return
                md = event.mimeData()
                if hasattr(md, 'column_name'):
                    fm_src = self.db.metadata_for_field(md.column_name)
                    if md.column_name in ['authors', 'publisher', 'series'] or \
                            (fm_src['is_custom'] and (
                             (fm_src['datatype'] in ['series', 'text', 'enumeration'] and not fm_src['is_multiple']) or (
                                 fm_src['datatype'] == 'composite' and fm_src['display'].get('make_category', False)))):
                        self.setDropIndicatorShown(True)

    def clear(self):
        if self.model():
            self.model().clear_state()

    def is_visible(self, idx):
        item = idx.data(Qt.UserRole)
        if getattr(item, 'type', None) == TagTreeItem.TAG:
            idx = idx.parent()
        return self.isExpanded(idx)

    def recount_with_position_based_index(self):
        self._model.use_position_based_index_on_next_recount = True
        self.recount()

    def recount(self, *args):
        '''
        Rebuild the category tree, expand any categories that were expanded,
        reset the search states, and reselect the current node.
        '''
        if self.disable_recounting or not self.pane_is_visible:
            return
        self.refresh_signal_processed = True
        ci = self.currentIndex()
        if not ci.isValid():
            ci = self.indexAt(QPoint(10, 10))
        use_pos = self._model.use_position_based_index_on_next_recount
        self._model.use_position_based_index_on_next_recount = False
        if use_pos:
            path = self._model.path_for_index(ci) if self.is_visible(ci) else None
        else:
            path = self._model.named_path_for_index(ci) if self.is_visible(ci) else None
        expanded_categories, state_map = self.get_state()
        self._model.rebuild_node_tree(state_map=state_map)
        self.blockSignals(True)
        for category in expanded_categories:
            idx = self._model.index_for_category(category)
            if idx is not None and idx.isValid():
                self.expand(idx)
        if path is not None:
            if use_pos:
                self.show_item_at_path(path)
            else:
                index = self._model.index_for_named_path(path)
                if index.isValid():
                    self.show_item_at_index(index)
        self.blockSignals(False)

    def show_item_at_path(self, path, box=False,
                          position=QTreeView.PositionAtCenter):
        '''
        Scroll the browser and open categories to show the item referenced by
        path. If possible, the item is placed in the center. If box=True, a
        box is drawn around the item.
        '''
        if path:
            self.show_item_at_index(self._model.index_for_path(path), box=box,
                                    position=position)

    def expand_parent(self, idx):
        # Needed otherwise Qt sometimes segfaults if the node is buried in a
        # collapsed, off screen hierarchy. To be safe, we expand from the
        # outermost in
        p = self._model.parent(idx)
        if p.isValid():
            self.expand_parent(p)
        self.expand(idx)

    def show_item_at_index(self, idx, box=False,
                           position=QTreeView.PositionAtCenter):
        if idx.isValid() and idx.data(Qt.UserRole) is not self._model.root_item:
            self.expand_parent(idx)
            self.setCurrentIndex(idx)
            self.scrollTo(idx, position)
            if box:
                self._model.set_boxed(idx)

    def item_expanded(self, idx):
        '''
        Called by the expanded signal
        '''
        self.setCurrentIndex(idx)
예제 #26
0
class Widget(QWidget, ScreenWidget):
    name = "manualPartitioning"

    def __init__(self):
        QWidget.__init__(self)
        self.ui = Ui_ManualPartWidget()
        self.ui.setupUi(self)
        self.storage = ctx.storage
        self.intf = ctx.interface

        # New Device Popup Menu
        self.setupMenu()

        #self.ui.newButtonclicked.connect(self.createDevice)
        self.ui.editButton.clicked.connect(self.editDevice)
        self.ui.deleteButton.clicked.connect(self.deleteDevice)
        self.ui.resetButton.clicked.connect(self.reset)
        self.ui.deviceTree.itemClicked[QTreeWidgetItem , int].connect(self.activateButtons)
        self.menu.triggered[QAction].connect(self.createDevice)

    def shown(self):
        checkForSwapNoMatch(self.intf, self.storage)
        self.populate()
        (errors, warnings) =  self.storage.sanityCheck()
        if errors or warnings:
            ctx.mainScreen.disableNext()
        else:
            ctx.mainScreen.enableNext()
        self.update()

    def execute(self):
        ctx.logger.info("Manual Partitioning selected...")
        ctx.mainScreen.processEvents()
        check = self.nextCheck()
        if not check:
            ctx.mainScreen.enableBack()
        elif check is None:
            ctx.mainScreen.enableNext()
        return check

    def update(self):
        if self.storage.storageset.rootDevice:
            ctx.mainScreen.enableNext()
        else:
            ctx.mainScreen.disableNext()
        self.updateMenus()

    def activateButtons(self, item, index):
        if item:
            if isinstance(item.device, Device) and not isinstance(item.device, parted.partition.Partition):
                self.ui.editButton.setEnabled(True)
                self.ui.deleteButton.setEnabled(True)
            else:
                self.ui.editButton.setEnabled(False)
                self.ui.deleteButton.setEnabled(False)

    def nextCheck(self):
        (errors, warnings) = self.storage.sanityCheck()
        if errors:
            detailed =  _("The partitioning scheme you requested "
                          "caused the following critical errors."
                          "You must correct these errors before "
                          "you continue your installation of %s.") \
                         % yali.util.product_name()

            comments = "\n\n".join(errors)
            self.intf.detailedMessageWindow(_("Partitioning Errors"),
                                             detailed, comments, type="error")
            return False

        if warnings:
            detailed = _("The partitioning scheme you requested generated the "
                         "following warnings. Would you like to continue with "
                         "your requested partitioning "
                         "scheme?")

            comments = "\n\n".join(warnings)
            rc = self.intf.detailedMessageWindow(_("Partitioning Warnings"),
                                                  detailed, comments, type="custom", customIcon="warning",
                                                  customButtons=[_("Ok"), _("Cancel")], default=1)
            if rc:
                return False

        formatWarnings = getPreExistFormatWarnings(self.storage)
        if formatWarnings:
            detailed = _("The following pre-existing devices have "
                         "been selected to be formatted, destroying "
                         "all data.")

            comments = ""
            for (device, type, mountpoint) in formatWarnings:
                comments = comments + "%s         %s         %s\n" % (device, type, mountpoint)

            rc = self.intf.detailedMessageWindow(_("Format Warnings"),
                                                  detailed, comments, type="custom", customIcon="warning",
                                                  customButtons=[_("Format"), _("Cancel")], default=1)
            if rc:
                return False

        return True


    def backCheck(self):
        rc = self.intf.messageWindow(_("Warning"), _("All Changes that you made will be removed"),
                                      type="custom", customIcon="warning",
                                      customButtons=[_("Ok"), _("Cancel")], default=1)
        if not rc:
            self.storage.reset()
            return True
        return False

    def setupMenu(self):
        self.menu = QMenu("New")
        self.standardDevices = self.menu.addMenu(_("Standard"))
        self.lvmDevices = self.menu.addMenu(_("LVM"))
        self.raidDevices = self.menu.addMenu(_("RAID"))

        self.createPartition = self.standardDevices.addAction(_("Partition"))
        self.createPartition.setWhatsThis(_("General purpose of partition creation"))
        self.createPartition.setVisible(False)
        self.createPhysicalVolume = self.lvmDevices.addAction(_("Physical Volume"))
        self.createPhysicalVolume.setWhatsThis(_("Create LVM formatted partition"))
        self.createPhysicalVolume.setVisible(False)
        self.createVolumeGroup = self.lvmDevices.addAction(_("Volume Group"))
        self.createVolumeGroup.setWhatsThis(_("Requires at least 1 free LVM formatted partition"))
        self.createVolumeGroup.setVisible(False)
        self.createLogicalVolume = self.lvmDevices.addAction(_("Logical Volume"))
        self.createLogicalVolume.setWhatsThis(_("Create Logical Volume on selected Volume Group"))
        self.createLogicalVolume.setVisible(False)
        self.createRaidMember = self.raidDevices.addAction(_("Member"))
        self.createRaidMember.setWhatsThis(_("Create Raid formatted partition"))
        self.createRaidMember.setVisible(False)
        self.createRaidArray= self.raidDevices.addAction(_("Array"))
        self.createRaidArray.setWhatsThis(_("Requires at least 2 free Raid formatted partition"))
        self.createRaidArray.setVisible(False)

        self.ui.newButton.setMenu(self.menu)

    def addDevice(self, device, item):
        if device.format.hidden:
            return

        format = device.format

        if not format.exists:
            formatIcon = QIcon(":/gui/pics/tick.png")
        else:
            #formatIcon = QIcon(":/gui/pics/dialog-error.png")
            formatIcon = QIcon("")

        # mount point string
        if format.type == "lvmpv":
            vg = None
            for _vg in self.storage.vgs:
                if _vg.dependsOn(device):
                    vg = _vg
                    break
            mountpoint = getattr(vg, "name", "")
        elif format.type == "mdmember":
            array = None
            for _array in self.storage.raidArrays:
                if _array.dependsOn(device):
                    array = _array
                    break

            mountpoint = getattr(array, "name", "")
        else:
            mountpoint = getattr(format, "mountpoint", "")
            if mountpoint is None:
                mountpoint = ""

        # device name
        name = getattr(device, "lvname", device.name)

        # label
        label = getattr(format, "label", "")
        if label is None:
            label = ""

        item.setDevice(device)
        item.setName(name)
        item.setMountpoint(mountpoint)
        item.setLabel(label)
        item.setType(format.name)
        item.setSize("%Ld" % device.size)
        item.setFormat(formatIcon)
        item.setExpanded(True)

    def populate(self):
        # Clear device tree
        self.ui.deviceTree.clear()

        # first do LVM
        vgs = self.storage.vgs
        if vgs:
            volumeGroupsItem = DeviceTreeItem(self.ui.deviceTree)
            volumeGroupsItem.setName(_("Volume Groups"))
            volumeGroupsItem.setExpanded(True)
            for vg in vgs:
                volumeGroupItem = DeviceTreeItem(volumeGroupsItem)
                self.addDevice(vg, volumeGroupItem)
                volumeGroupItem.setType("")
                for lv in vg.lvs:
                    logicalVolumeItem = DeviceTreeItem(volumeGroupItem)
                    self.addDevice(lv, logicalVolumeItem)

                # We add a row for the VG free space.
                if vg.freeSpace > 0:
                    freeLogicalVolumeItem = DeviceTreeItem(volumeGroupItem)
                    freeLogicalVolumeItem.setName(_("Free"))
                    freeLogicalVolumeItem.setSize("%Ld" % vg.freeSpace)
                    freeLogicalVolumeItem.setDevice(None)
                    freeLogicalVolumeItem.setMountpoint("")

        # handle RAID next
        raidarrays = self.storage.raidArrays
        if raidarrays:
            raidArraysItem = DeviceTreeItem(self.ui.deviceTree)
            raidArraysItem.setName(_("Raid Arrays"))
            raidArraysItem.setExpanded(True)
            for array in raidarrays:
                raidArrayItem = DeviceTreeItem(raidArraysItem)
                self.addDevice(array, raidArrayItem)

        # now normal partitions
        disks = self.storage.partitioned
        # also include unpartitioned disks that aren't mpath or biosraid
        whole = filter(lambda d: not d.partitioned and not d.format.hidden,
                       self.storage.disks)
        disks.extend(whole)
        disks.sort(key=lambda d: d.name)
        # Disk&Partitions
        drivesItem = DeviceTreeItem(self.ui.deviceTree)
        drivesItem.setName(_("Hard Drives"))
        drivesItem.setExpanded(True)
        for disk in disks:
            diskItem = DeviceTreeItem(drivesItem)
            diskItem.setExpanded(True)
            diskItem.setName("%s - %s" % (disk.model, disk.name))
            #self.ui.deviceTree.expandItem(diskItem)
            if disk.partitioned:
                partition = disk.format.firstPartition
                extendedItem = None
                while partition:
                    if partition.type & parted.PARTITION_METADATA:
                        partition = partition.nextPartition()
                        continue

                    partName = devicePathToName(partition.getDeviceNodeName())
                    device = self.storage.devicetree.getDeviceByName(partName)

                    if not device and not partition.type & parted.PARTITION_FREESPACE:
                        ctx.logger.debug("can't find partition %s in device tree" % partName)

                    # Force partitions tree item not to be less than 12 MB
                    if partition.getSize(unit="MB") <= 12.0:
                        if not partition.active or not partition.getFlag(parted.PARTITION_BOOT):
                            partition = partition.nextPartition()
                            continue

                    if device and device.isExtended:
                        if extendedItem:
                            raise RuntimeError, _("Can't handle more than "
                                                 "one extended partition per disk")
                        extendedItem = partItem = DeviceTreeItem(diskItem)
                        partitionItem = extendedItem

                    elif device and device.isLogical:
                        if not extendedItem:
                            raise RuntimeError, _("Crossed logical partition before extended")
                        partitionItem = DeviceTreeItem(extendedItem)

                    else:
                        # Free space item
                        if partition.type & parted.PARTITION_LOGICAL:
                            partitionItem = DeviceTreeItem(extendedItem)
                        else:
                            partitionItem = DeviceTreeItem(diskItem)


                    if device and not device.isExtended:
                        self.addDevice(device, partitionItem)
                    else:
                        # either extended or freespace
                        if partition.type & parted.PARTITION_FREESPACE:
                            deviceName = _("Free")
                            device = partition
                            deviceType = ""
                        else:
                            deviceName = device.name
                            deviceType = _("Extended")

                        partitionItem.setName(deviceName)
                        partitionItem.setType(deviceType)
                        size = partition.getSize(unit="MB")
                        if size < 1.0:
                            size = "< 1"
                        else:
                            size = "%Ld" % (size)
                        partitionItem.setSize(size)
                        partitionItem.setDevice(device)

                    partition = partition.nextPartition()
            else:
                self.addDevice(disk, diskItem)

    def refresh(self, justRedraw=None):
        ctx.logger.debug("refresh: justRedraw=%s" % justRedraw)
        self.ui.deviceTree.clear()
        if justRedraw:
            rc = 0
        else:
            try:
                doPartitioning(self.storage)
                rc = 0
            except PartitioningError, msg:
                self.intf.messageWindow(_("Error Partitioning"), 
                                        _("Could not allocate requested partitions: %s.") % msg,
                                        customIcon="error")
                rc = -1
            except PartitioningWarning, msg:
                rc = self.intf.messageWindow(_("Warning Partitioning"),
                                             _("Warning: %s.") % msg,
                                             customButtons=[_("Modify Partition"), _("Continue")],
                                             customIcon="warning")
                if rc == 1:
                    rc = -1
                else:
                    rc = 0
                    all_devices = self.storage.devicetree.devices
                    bootDevs = [d for d in all_devices if d.bootable]
예제 #27
0
파일: widget.py 프로젝트: elonchen/calibre
    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction
        c = self.editor.cursorForPosition(pos)
        origc = QTextCursor(c)
        current_cursor = self.editor.textCursor()
        r = origr = self.editor.syntax_range_for_cursor(c)
        if (
                r is None or not r.format.property(SPELL_PROPERTY)
        ) and c.positionInBlock() > 0 and not current_cursor.hasSelection():
            c.setPosition(c.position() - 1)
            r = self.editor.syntax_range_for_cursor(c)

        if r is not None and r.format.property(SPELL_PROPERTY):
            word = self.editor.text_for_range(c.block(), r)
            locale = self.editor.spellcheck_locale_for_cursor(c)
            orig_pos = c.position()
            c.setPosition(orig_pos - utf16_length(word))
            found = False
            self.editor.setTextCursor(c)
            if self.editor.find_spell_word([word],
                                           locale.langcode,
                                           center_on_cursor=False):
                found = True
                fc = self.editor.textCursor()
                if fc.position() < c.position():
                    self.editor.find_spell_word([word],
                                                locale.langcode,
                                                center_on_cursor=False)
            spell_cursor = self.editor.textCursor()
            if current_cursor.hasSelection():
                # Restore the current cursor so that any selection is preserved
                # for the change case actions
                self.editor.setTextCursor(current_cursor)
            if found:
                suggestions = dictionaries.suggestions(word, locale)[:7]
                if suggestions:
                    for suggestion in suggestions:
                        ac = m.addAction(
                            suggestion,
                            partial(self.editor.simple_replace,
                                    suggestion,
                                    cursor=spell_cursor))
                        f = ac.font()
                        f.setBold(True), ac.setFont(f)
                    m.addSeparator()
                m.addAction(actions['spell-next'])
                m.addAction(_('Ignore this word'),
                            partial(self._nuke_word, None, word, locale))
                dics = dictionaries.active_user_dictionaries
                if len(dics) > 0:
                    if len(dics) == 1:
                        m.addAction(
                            _('Add this word to the dictionary: {0}').format(
                                dics[0].name),
                            partial(self._nuke_word, dics[0].name, word,
                                    locale))
                    else:
                        ac = m.addAction(_('Add this word to the dictionary'))
                        dmenu = QMenu(m)
                        ac.setMenu(dmenu)
                        for dic in dics:
                            dmenu.addAction(
                                dic.name,
                                partial(self._nuke_word, dic.name, word,
                                        locale))
                m.addSeparator()

        if origr is not None and origr.format.property(LINK_PROPERTY):
            href = self.editor.text_for_range(origc.block(), origr)
            m.addAction(
                _('Open %s') % href, partial(self.link_clicked.emit, href))

        if origr is not None and (origr.format.property(TAG_NAME_PROPERTY)
                                  or origr.format.property(CSS_PROPERTY)):
            word = self.editor.text_for_range(origc.block(), origr)
            item_type = 'tag_name' if origr.format.property(
                TAG_NAME_PROPERTY) else 'css_property'
            url = help_url(word,
                           item_type,
                           self.editor.highlighter.doc_name,
                           extra_data=current_container().opf_version)
            if url is not None:
                m.addAction(
                    _('Show help for: %s') % word, partial(open_url, url))

        for x in ('undo', 'redo'):
            ac = actions['editor-%s' % x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        for x in ('cut', 'copy', 'paste'):
            ac = actions['editor-' + x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        m.addAction(_('&Select all'), self.editor.select_all)
        if self.selected_text or self.has_marked_text:
            update_mark_text_action(self)
            m.addAction(actions['mark-selected-text'])
        if self.syntax != 'css' and actions['editor-cut'].isEnabled():
            cm = QMenu(_('Change &case'), m)
            for ac in 'upper lower swap title capitalize'.split():
                cm.addAction(actions['transform-case-' + ac])
            m.addMenu(cm)
        if self.syntax == 'html':
            m.addAction(actions['multisplit'])
        m.exec_(self.editor.viewport().mapToGlobal(pos))
    def init_stiffness_ctrl(self):
        
        slayout = QVBoxLayout()
        
        prefix = "meka_roscontrol"
        suffix = "stiffness_controller/command"
        
        button = QPushButton("Stiffness controller (beta)")

        self.stiffness_pub = rospy.Publisher("/" + prefix + "/" + suffix, Float64MultiArray, queue_size=1)
        group_names = ["right_arm", "left_arm", "right_hand", "left_hand", "head", "torso", "zlift"]
        # slider for each group
        joint_names = [ "right_arm_j0",
                        "right_arm_j1",
                        "right_arm_j2",
                        "right_arm_j3",
                        "right_arm_j4",
                        "right_arm_j5",
                        "right_arm_j6",
                        "left_arm_j0",
                        "left_arm_j1",
                        "left_arm_j2",
                        "left_arm_j3",
                        "left_arm_j4",
                        "left_arm_j5",
                        "left_arm_j6",
                        "right_hand_j0",
                        "right_hand_j1",
                        "right_hand_j2",
                        "right_hand_j3",
                        "right_hand_j4",
                        "left_hand_j0",
                        "left_hand_j1",
                        "left_hand_j2",
                        "left_hand_j3",
                        "left_hand_j4",
                        "head_j0",
                        "head_j1",
                        "torso_j0",
                        "torso_j1",
                        "zlift_j0"]
        
        self._stiffness_dict = OrderedDict((name, 1.0) for name in joint_names)
        
        menu = QMenu("Menu")
        menu.setStyleSheet("QMenu { menu-scrollable: 1; }");
        self.stiffnessvals = {}
        for group in group_names:
            glayout = QHBoxLayout()
            glayout.addWidget(QLabel(group))
            slider = QSlider(Qt.Horizontal)
            slider.setRange(0,100)
            slider.setValue(100)
            slider.setTickPosition(QSlider.TicksBelow)
            slider.setTickInterval(10)
            slider.setFixedSize(200,15)
            slider.setSingleStep(10)
            slider.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
            
            val = QLabel("1.0")
            btn = QPushButton("apply")
            self.stiffnessvals[group] = val
            btn.clicked.connect(partial(self.on_stiffness_apply, group))
            
            glayout.addWidget(slider)
            glayout.addWidget(val)
            glayout.addWidget(btn)
            
            slider.valueChanged.connect(partial(self.on_stiffness_change, slider, val, group))
            
            slayout.addLayout(glayout)
            
            
            groupm = menu.addMenu(group)
            #for joint in joint_names:
                #groupm.addAction("set stiffness", partial(self.set_stiffness, groupm))
                #if group in joint:
                #    groupm.addAction(self._stiffness_dict[joint])
                #    s.addAction(joint, partial(self.request_fields, component, s))
        
        

        button.setMenu(menu)

        return slayout
예제 #29
0
class RulesDialog(Dialog):

    def __init__(self, parent=None):
        self.loaded_ruleset = None
        Dialog.__init__(self, _('Edit tag mapper rules'), 'edit-tag-mapper-rules', parent=parent)

    def setup_ui(self):
        self.l = l = QVBoxLayout(self)
        self.edit_widget = w = Rules(self)
        l.addWidget(w)
        l.addWidget(self.bb)
        self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole)
        b.setToolTip(_('Save this ruleset for later re-use'))
        b.clicked.connect(self.save_ruleset)
        self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole)
        b.setToolTip(_('Load a previously saved ruleset'))
        self.load_menu = QMenu(self)
        b.setMenu(self.load_menu)
        self.build_load_menu()
        self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole)
        b.clicked.connect(self.test_rules)

    @property
    def rules(self):
        return self.edit_widget.rules

    @rules.setter
    def rules(self, rules):
        self.edit_widget.rules = rules

    def save_ruleset(self):
        if not self.rules:
            error_dialog(self, _('No rules'), _(
                'Cannot save as no rules have been created'), show=True)
            return
        text, ok = QInputDialog.getText(self, _('Save ruleset as'), _(
            'Enter a name for this ruleset:'), text=self.loaded_ruleset or '')
        if ok and text:
            if self.loaded_ruleset and text == self.loaded_ruleset:
                if not question_dialog(self, _('Are you sure?'), _(
                        'A ruleset with the name "%s" already exists, do you want to replace it?') % text):
                    return
                self.loaded_ruleset = text
            rules = self.rules
            if rules:
                tag_maps[text] = self.rules
            elif text in tag_maps:
                del tag_maps[text]
            self.build_load_menu()

    def build_load_menu(self):
        self.load_menu.clear()
        if len(tag_maps):
            for name, rules in tag_maps.iteritems():
                self.load_menu.addAction(name).triggered.connect(partial(self.load_ruleset, name))
            self.load_menu.addSeparator()
            m = self.load_menu.addMenu(_('Delete saved rulesets'))
            for name, rules in tag_maps.iteritems():
                m.addAction(name).triggered.connect(partial(self.delete_ruleset, name))
        else:
            self.load_menu.addAction(_('No saved rulesets available'))

    def load_ruleset(self, name):
        self.rules = tag_maps[name]
        self.loaded_ruleset = name

    def delete_ruleset(self, name):
        del tag_maps[name]
        self.build_load_menu()

    def test_rules(self):
        Tester(self.rules, self).exec_()
예제 #30
0
class RulesDialog(Dialog):

    def __init__(self, parent=None):
        self.loaded_ruleset = None
        Dialog.__init__(self, _('Edit tag mapper rules'), 'edit-tag-mapper-rules', parent=parent)

    def setup_ui(self):
        self.l = l = QVBoxLayout(self)
        self.edit_widget = w = Rules(self)
        l.addWidget(w)
        l.addWidget(self.bb)
        self.save_button = b = self.bb.addButton(_('&Save'), self.bb.ActionRole)
        b.setToolTip(_('Save this ruleset for later re-use'))
        b.clicked.connect(self.save_ruleset)
        self.load_button = b = self.bb.addButton(_('&Load'), self.bb.ActionRole)
        b.setToolTip(_('Load a previously saved ruleset'))
        self.load_menu = QMenu(self)
        b.setMenu(self.load_menu)
        self.build_load_menu()
        self.test_button = b = self.bb.addButton(_('&Test rules'), self.bb.ActionRole)
        b.clicked.connect(self.test_rules)

    @property
    def rules(self):
        return self.edit_widget.rules

    @rules.setter
    def rules(self, rules):
        self.edit_widget.rules = rules

    def save_ruleset(self):
        if not self.rules:
            error_dialog(self, _('No rules'), _(
                'Cannot save as no rules have been created'), show=True)
            return
        text, ok = QInputDialog.getText(self, _('Save ruleset as'), _(
            'Enter a name for this ruleset:'), text=self.loaded_ruleset or '')
        if ok and text:
            if self.loaded_ruleset and text == self.loaded_ruleset:
                if not question_dialog(self, _('Are you sure?'), _(
                        'A ruleset with the name "%s" already exists, do you want to replace it?') % text):
                    return
                self.loaded_ruleset = text
            rules = self.rules
            if rules:
                tag_maps[text] = self.rules
            elif text in tag_maps:
                del tag_maps[text]
            self.build_load_menu()

    def build_load_menu(self):
        self.load_menu.clear()
        if len(tag_maps):
            for name, rules in tag_maps.iteritems():
                self.load_menu.addAction(name).triggered.connect(partial(self.load_ruleset, name))
            self.load_menu.addSeparator()
            m = self.load_menu.addMenu(_('Delete saved rulesets'))
            for name, rules in tag_maps.iteritems():
                m.addAction(name).triggered.connect(partial(self.delete_ruleset, name))
        else:
            self.load_menu.addAction(_('No saved rulesets available'))

    def load_ruleset(self, name):
        self.rules = tag_maps[name]
        self.loaded_ruleset = name

    def delete_ruleset(self, name):
        del tag_maps[name]
        self.build_load_menu()

    def test_rules(self):
        Tester(self.rules, self).exec_()
예제 #31
0
class TagsView(QTreeView):  # {{{

    refresh_required        = pyqtSignal()
    tags_marked             = pyqtSignal(object)
    edit_user_category      = pyqtSignal(object)
    delete_user_category    = pyqtSignal(object)
    del_item_from_user_cat  = pyqtSignal(object, object, object)
    add_item_to_user_cat    = pyqtSignal(object, object, object)
    add_subcategory         = pyqtSignal(object)
    tags_list_edit          = pyqtSignal(object, object)
    saved_search_edit       = pyqtSignal(object)
    rebuild_saved_searches  = pyqtSignal()
    author_sort_edit        = pyqtSignal(object, object, object, object)
    tag_item_renamed        = pyqtSignal()
    search_item_renamed     = pyqtSignal()
    drag_drop_finished      = pyqtSignal(object)
    restriction_error       = pyqtSignal()
    tag_item_delete         = pyqtSignal(object, object, object, object)

    def __init__(self, parent=None):
        QTreeView.__init__(self, parent=None)
        self.setMouseTracking(True)
        self.alter_tb = None
        self.disable_recounting = False
        self.setUniformRowHeights(True)
        self.setIconSize(QSize(20, 20))
        self.setTabKeyNavigation(True)
        self.setAnimated(True)
        self.setHeaderHidden(True)
        self.setItemDelegate(TagDelegate(self))
        self.made_connections = False
        self.setAcceptDrops(True)
        self.setDragEnabled(True)
        self.setDragDropMode(self.DragDrop)
        self.setDropIndicatorShown(True)
        self.in_drag_drop = False
        self.setAutoExpandDelay(500)
        self.pane_is_visible = False
        self.search_icon = QIcon(I('search.png'))
        self.user_category_icon = QIcon(I('tb_folder.png'))
        self.delete_icon = QIcon(I('list_remove.png'))
        self.rename_icon = QIcon(I('edit-undo.png'))

        self._model = TagsModel(self)
        self._model.search_item_renamed.connect(self.search_item_renamed)
        self._model.refresh_required.connect(self.refresh_required,
                type=Qt.QueuedConnection)
        self._model.tag_item_renamed.connect(self.tag_item_renamed)
        self._model.restriction_error.connect(self.restriction_error)
        self._model.user_categories_edited.connect(self.user_categories_edited,
                type=Qt.QueuedConnection)
        self._model.drag_drop_finished.connect(self.drag_drop_finished)
        stylish_tb = '''
                QTreeView {
                    background-color: palette(window);
                    color: palette(window-text);
                    border: none;
                }
        '''
        self.setStyleSheet('''
                QTreeView::item {
                    border: 1px solid transparent;
                    padding-top:0.8ex;
                    padding-bottom:0.8ex;
                }

                QTreeView::item:hover {
                    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #e7effd, stop: 1 #cbdaf1);
                    border: 1px solid #bfcde4;
                    border-radius: 6px;
                }
        ''' + ('' if gprefs['tag_browser_old_look'] else stylish_tb))
        if gprefs['tag_browser_old_look']:
            self.setAlternatingRowColors(True)
        # Allowing keyboard focus looks bad in the Qt Fusion style and is useless
        # anyway since the enter/spacebar keys do nothing
        self.setFocusPolicy(Qt.NoFocus)

    @property
    def hidden_categories(self):
        return self._model.hidden_categories

    @property
    def db(self):
        return self._model.db

    @property
    def collapse_model(self):
        return self._model.collapse_model

    def set_pane_is_visible(self, to_what):
        pv = self.pane_is_visible
        self.pane_is_visible = to_what
        if to_what and not pv:
            self.recount()

    def get_state(self):
        state_map = {}
        expanded_categories = []
        for row, category in enumerate(self._model.category_nodes):
            if self.isExpanded(self._model.index(row, 0, QModelIndex())):
                expanded_categories.append(category.category_key)
            states = [c.tag.state for c in category.child_tags()]
            names = [(c.tag.name, c.tag.category) for c in category.child_tags()]
            state_map[category.category_key] = dict(izip(names, states))
        return expanded_categories, state_map

    def reread_collapse_parameters(self):
        self._model.reread_collapse_model(self.get_state()[1])

    def set_database(self, db, alter_tb):
        self._model.set_database(db)
        self.alter_tb = alter_tb
        self.pane_is_visible = True  # because TagsModel.set_database did a recount
        self.setModel(self._model)
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        pop = self.db.CATEGORY_SORTS.index(config['sort_tags_by'])
        self.alter_tb.sort_menu.actions()[pop].setChecked(True)
        try:
            match_pop = self.db.MATCH_TYPE.index(config['match_tags_type'])
        except ValueError:
            match_pop = 0
        self.alter_tb.match_menu.actions()[match_pop].setChecked(True)
        if not self.made_connections:
            self.clicked.connect(self.toggle)
            self.customContextMenuRequested.connect(self.show_context_menu)
            self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
            self.alter_tb.sort_menu.triggered.connect(self.sort_changed)
            self.alter_tb.match_menu.triggered.connect(self.match_changed)
            self.made_connections = True
        self.refresh_signal_processed = True
        db.add_listener(self.database_changed)
        self.expanded.connect(self.item_expanded)

    def database_changed(self, event, ids):
        if self.refresh_signal_processed:
            self.refresh_signal_processed = False
            self.refresh_required.emit()

    def user_categories_edited(self, user_cats, nkey):
        state_map = self.get_state()[1]
        self.db.new_api.set_pref('user_categories', user_cats)
        self._model.rebuild_node_tree(state_map=state_map)
        p = self._model.find_category_node('@'+nkey)
        self.show_item_at_path(p)

    @property
    def match_all(self):
        return (self.alter_tb and
                self.alter_tb.match_menu.actions()[1].isChecked())

    def sort_changed(self, action):
        for i, ac in enumerate(self.alter_tb.sort_menu.actions()):
            if ac is action:
                config.set('sort_tags_by', self.db.CATEGORY_SORTS[i])
                self.recount()
                break

    def match_changed(self, action):
        try:
            for i, ac in enumerate(self.alter_tb.match_menu.actions()):
                if ac is action:
                    config.set('match_tags_type', self.db.MATCH_TYPE[i])
        except:
            pass

    def mouseMoveEvent(self, event):
        dex = self.indexAt(event.pos())
        if dex.isValid():
            self.setCursor(Qt.PointingHandCursor)
        else:
            self.unsetCursor()
        if not event.buttons() & Qt.LeftButton:
            return
        if self.in_drag_drop or not dex.isValid():
            QTreeView.mouseMoveEvent(self, event)
            return
        # Must deal with odd case where the node being dragged is 'virtual',
        # created to form a hierarchy. We can't really drag this node, but in
        # addition we can't allow drag recognition to notice going over some
        # other node and grabbing that one. So we set in_drag_drop to prevent
        # this from happening, turning it off when the user lifts the button.
        self.in_drag_drop = True
        if not self._model.flags(dex) & Qt.ItemIsDragEnabled:
            QTreeView.mouseMoveEvent(self, event)
            return
        md = self._model.mimeData([dex])
        pixmap = dex.data(DRAG_IMAGE_ROLE).pixmap(self.iconSize())
        drag = QDrag(self)
        drag.setPixmap(pixmap)
        drag.setMimeData(md)
        if self._model.is_in_user_category(dex):
            drag.exec_(Qt.CopyAction|Qt.MoveAction, Qt.CopyAction)
        else:
            drag.exec_(Qt.CopyAction)

    def mouseReleaseEvent(self, event):
        # Swallow everything except leftButton so context menus work correctly
        if event.button() == Qt.LeftButton or self.in_drag_drop:
            QTreeView.mouseReleaseEvent(self, event)
            self.in_drag_drop = False

    def mouseDoubleClickEvent(self, event):
        # swallow these to avoid toggling and editing at the same time
        pass

    @property
    def search_string(self):
        tokens = self._model.tokens()
        joiner = ' and ' if self.match_all else ' or '
        return joiner.join(tokens)

    def toggle(self, index):
        self._toggle(index, None)

    def _toggle(self, index, set_to):
        '''
        set_to: if None, advance the state. Otherwise must be one of the values
        in TAG_SEARCH_STATES
        '''
        modifiers = int(QApplication.keyboardModifiers())
        exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
        if self._model.toggle(index, exclusive, set_to=set_to):
            self.tags_marked.emit(self.search_string)

    def conditional_clear(self, search_string):
        if search_string != self.search_string:
            self.clear()

    def context_menu_handler(self, action=None, category=None,
                             key=None, index=None, search_state=None,
                             use_vl=None):
        if not action:
            return
        try:
            if action == 'set_icon':
                try:
                    path = choose_files(self, 'choose_category_icon',
                                _('Change Icon for: %s')%key, filters=[
                                ('Images', ['png', 'gif', 'jpg', 'jpeg'])],
                            all_files=False, select_only_single_file=True)
                    if path:
                        path = path[0]
                        p = QIcon(path).pixmap(QSize(128, 128))
                        d = os.path.join(config_dir, 'tb_icons')
                        if not os.path.exists(d):
                            os.makedirs(d)
                        with open(os.path.join(d, 'icon_'+
                            sanitize_file_name_unicode(key)+'.png'), 'wb') as f:
                            f.write(pixmap_to_data(p, format='PNG'))
                            path = os.path.basename(f.name)
                        self._model.set_custom_category_icon(key, unicode(path))
                        self.recount()
                except:
                    import traceback
                    traceback.print_exc()
                return
            if action == 'clear_icon':
                self._model.set_custom_category_icon(key, None)
                self.recount()
                return

            if action == 'edit_item_no_vl':
                item = self.model().get_node(index)
                item.use_vl = False
                self.edit(index)
                return
            if action == 'edit_item_in_vl':
                item = self.model().get_node(index)
                item.use_vl = True
                self.edit(index)
                return
            if action == 'delete_item_in_vl':
                self.tag_item_delete.emit(key, index.id, index.original_name,
                                          self.model().get_book_ids_to_use())
                return
            if action == 'delete_item_no_vl':
                self.tag_item_delete.emit(key, index.id, index.original_name, None)
                return
            if action == 'open_editor':
                self.tags_list_edit.emit(category, key)
                return
            if action == 'manage_categories':
                self.edit_user_category.emit(category)
                return
            if action == 'search':
                self._toggle(index, set_to=search_state)
                return
            if action == 'add_to_category':
                tag = index.tag
                if len(index.children) > 0:
                    for c in index.all_children():
                        self.add_item_to_user_cat.emit(category, c.tag.original_name,
                                               c.tag.category)
                self.add_item_to_user_cat.emit(category, tag.original_name,
                                               tag.category)
                return
            if action == 'add_subcategory':
                self.add_subcategory.emit(key)
                return
            if action == 'search_category':
                self._toggle(index, set_to=search_state)
                return
            if action == 'delete_user_category':
                self.delete_user_category.emit(key)
                return
            if action == 'delete_search':
                self.model().db.saved_search_delete(key)
                self.rebuild_saved_searches.emit()
                return
            if action == 'delete_item_from_user_category':
                tag = index.tag
                if len(index.children) > 0:
                    for c in index.children:
                        self.del_item_from_user_cat.emit(key, c.tag.original_name,
                                               c.tag.category)
                self.del_item_from_user_cat.emit(key, tag.original_name, tag.category)
                return
            if action == 'manage_searches':
                self.saved_search_edit.emit(category)
                return
            if action == 'edit_author_sort':
                self.author_sort_edit.emit(self, index, True, False)
                return
            if action == 'edit_author_link':
                self.author_sort_edit.emit(self, index, False, True)
                return

            reset_filter_categories = True
            if action == 'hide':
                self.hidden_categories.add(category)
            elif action == 'show':
                self.hidden_categories.discard(category)
            elif action == 'categorization':
                changed = self.collapse_model != category
                self._model.collapse_model = category
                if changed:
                    reset_filter_categories = False
                    gprefs['tags_browser_partition_method'] = category
            elif action == 'defaults':
                self.hidden_categories.clear()
            self.db.new_api.set_pref('tag_browser_hidden_categories', list(self.hidden_categories))
            if reset_filter_categories:
                self._model.set_categories_filter(None)
            self._model.rebuild_node_tree()
        except:
            return

    def show_context_menu(self, point):
        def display_name(tag):
            if tag.category == 'search':
                n = tag.name
                if len(n) > 45:
                    n = n[:45] + '...'
                return "'" + n + "'"
            return tag.name

        index = self.indexAt(point)
        self.context_menu = QMenu(self)

        if index.isValid():
            item = index.data(Qt.UserRole)
            tag = None

            if item.type == TagTreeItem.TAG:
                tag_item = item
                tag = item.tag
                while item.type != TagTreeItem.CATEGORY:
                    item = item.parent

            if item.type == TagTreeItem.CATEGORY:
                if not item.category_key.startswith('@'):
                    while item.parent != self._model.root_item:
                        item = item.parent
                category = unicode(item.name or '')
                key = item.category_key
                # Verify that we are working with a field that we know something about
                if key not in self.db.field_metadata:
                    return True

                # Did the user click on a leaf node?
                if tag:
                    # If the user right-clicked on an editable item, then offer
                    # the possibility of renaming that item.
                    if tag.is_editable:
                        # Add the 'rename' items
                        if self.model().get_in_vl():
                            self.context_menu.addAction(self.rename_icon,
                                                    _('Rename %s in virtual library')%display_name(tag),
                                    partial(self.context_menu_handler, action='edit_item_in_vl',
                                            index=index))
                        self.context_menu.addAction(self.rename_icon,
                                                _('Rename %s')%display_name(tag),
                                partial(self.context_menu_handler, action='edit_item_no_vl',
                                        index=index))
                        if key in ('tags', 'series', 'publisher') or \
                                self._model.db.field_metadata.is_custom_field(key):
                            if self.model().get_in_vl():
                                self.context_menu.addAction(self.delete_icon,
                                                    _('Delete %s in virtual library')%display_name(tag),
                                partial(self.context_menu_handler, action='delete_item_in_vl',
                                    key=key, index=tag))

                            self.context_menu.addAction(self.delete_icon,
                                                    _('Delete %s')%display_name(tag),
                                partial(self.context_menu_handler, action='delete_item_no_vl',
                                    key=key, index=tag))
                        if key == 'authors':
                            self.context_menu.addAction(_('Edit sort for %s')%display_name(tag),
                                    partial(self.context_menu_handler,
                                            action='edit_author_sort', index=tag.id))
                            self.context_menu.addAction(_('Edit link for %s')%display_name(tag),
                                    partial(self.context_menu_handler,
                                            action='edit_author_link', index=tag.id))

                        # is_editable is also overloaded to mean 'can be added
                        # to a user category'
                        m = self.context_menu.addMenu(self.user_category_icon,
                                        _('Add %s to user category')%display_name(tag))
                        nt = self.model().category_node_tree
                        def add_node_tree(tree_dict, m, path):
                            p = path[:]
                            for k in sorted(tree_dict.keys(), key=sort_key):
                                p.append(k)
                                n = k[1:] if k.startswith('@') else k
                                m.addAction(self.user_category_icon, n,
                                    partial(self.context_menu_handler,
                                            'add_to_category',
                                            category='.'.join(p), index=tag_item))
                                if len(tree_dict[k]):
                                    tm = m.addMenu(self.user_category_icon,
                                                   _('Children of %s')%n)
                                    add_node_tree(tree_dict[k], tm, p)
                                p.pop()
                        add_node_tree(nt, m, [])
                    elif key == 'search' and tag.is_searchable:
                        self.context_menu.addAction(self.rename_icon,
                                                    _('Rename %s')%display_name(tag),
                            partial(self.context_menu_handler, action='edit_item_no_vl',
                                    index=index))
                        self.context_menu.addAction(self.delete_icon,
                                _('Delete search %s')%display_name(tag),
                                partial(self.context_menu_handler,
                                        action='delete_search', key=tag.original_name))
                    if key.startswith('@') and not item.is_gst:
                        self.context_menu.addAction(self.user_category_icon,
                            _('Remove %(item)s from category %(cat)s')%
                            dict(item=display_name(tag), cat=item.py_name),
                            partial(self.context_menu_handler,
                                    action='delete_item_from_user_category',
                                    key=key, index=tag_item))
                    if tag.is_searchable:
                        # Add the search for value items. All leaf nodes are searchable
                        self.context_menu.addAction(self.search_icon,
                                _('Search for %s')%display_name(tag),
                                partial(self.context_menu_handler, action='search',
                                        search_state=TAG_SEARCH_STATES['mark_plus'],
                                        index=index))
                        self.context_menu.addAction(self.search_icon,
                                _('Search for everything but %s')%display_name(tag),
                                partial(self.context_menu_handler, action='search',
                                        search_state=TAG_SEARCH_STATES['mark_minus'],
                                        index=index))
                    self.context_menu.addSeparator()
                elif key.startswith('@') and not item.is_gst:
                    if item.can_be_edited:
                        self.context_menu.addAction(self.rename_icon,
                            _('Rename %s')%item.py_name,
                            partial(self.context_menu_handler, action='edit_item_no_vl',
                                    index=index))
                    self.context_menu.addAction(self.user_category_icon,
                            _('Add sub-category to %s')%item.py_name,
                            partial(self.context_menu_handler,
                                    action='add_subcategory', key=key))
                    self.context_menu.addAction(self.delete_icon,
                            _('Delete user category %s')%item.py_name,
                            partial(self.context_menu_handler,
                                    action='delete_user_category', key=key))
                    self.context_menu.addSeparator()
                # Hide/Show/Restore categories
                self.context_menu.addAction(_('Hide category %s') % category,
                    partial(self.context_menu_handler, action='hide',
                            category=key))
                if self.hidden_categories:
                    m = self.context_menu.addMenu(_('Show category'))
                    for col in sorted(self.hidden_categories,
                            key=lambda x: sort_key(self.db.field_metadata[x]['name'])):
                        m.addAction(self.db.field_metadata[col]['name'],
                            partial(self.context_menu_handler, action='show', category=col))

                # search by category. Some categories are not searchable, such
                # as search and news
                if item.tag.is_searchable:
                    self.context_menu.addAction(self.search_icon,
                            _('Search for books in category %s')%category,
                            partial(self.context_menu_handler,
                                    action='search_category',
                                    index=self._model.createIndex(item.row(), 0, item),
                                    search_state=TAG_SEARCH_STATES['mark_plus']))
                    self.context_menu.addAction(self.search_icon,
                            _('Search for books not in category %s')%category,
                            partial(self.context_menu_handler,
                                    action='search_category',
                                    index=self._model.createIndex(item.row(), 0, item),
                                    search_state=TAG_SEARCH_STATES['mark_minus']))
                # Offer specific editors for tags/series/publishers/saved searches
                self.context_menu.addSeparator()
                if key in ['tags', 'publisher', 'series'] or \
                            (self.db.field_metadata[key]['is_custom'] and
                             self.db.field_metadata[key]['datatype'] != 'composite'):
                    self.context_menu.addAction(_('Manage %s')%category,
                            partial(self.context_menu_handler, action='open_editor',
                                    category=tag.original_name if tag else None,
                                    key=key))
                elif key == 'authors':
                    self.context_menu.addAction(_('Manage %s')%category,
                            partial(self.context_menu_handler, action='edit_author_sort'))
                elif key == 'search':
                    self.context_menu.addAction(_('Manage Saved Searches'),
                        partial(self.context_menu_handler, action='manage_searches',
                                category=tag.name if tag else None))

                self.context_menu.addSeparator()
                self.context_menu.addAction(_('Change category icon'),
                        partial(self.context_menu_handler, action='set_icon', key=key))
                self.context_menu.addAction(_('Restore default icon'),
                        partial(self.context_menu_handler, action='clear_icon', key=key))

                # Always show the user categories editor
                self.context_menu.addSeparator()
                if key.startswith('@') and \
                        key[1:] in self.db.prefs.get('user_categories', {}).keys():
                    self.context_menu.addAction(_('Manage User Categories'),
                            partial(self.context_menu_handler, action='manage_categories',
                                    category=key[1:]))
                else:
                    self.context_menu.addAction(_('Manage User Categories'),
                            partial(self.context_menu_handler, action='manage_categories',
                                    category=None))

        if self.hidden_categories:
            if not self.context_menu.isEmpty():
                self.context_menu.addSeparator()
            self.context_menu.addAction(_('Show all categories'),
                        partial(self.context_menu_handler, action='defaults'))

        m = self.context_menu.addMenu(_('Change sub-categorization scheme'))
        da = m.addAction(_('Disable'),
            partial(self.context_menu_handler, action='categorization', category='disable'))
        fla = m.addAction(_('By first letter'),
            partial(self.context_menu_handler, action='categorization', category='first letter'))
        pa = m.addAction(_('Partition'),
            partial(self.context_menu_handler, action='categorization', category='partition'))
        if self.collapse_model == 'disable':
            da.setCheckable(True)
            da.setChecked(True)
        elif self.collapse_model == 'first letter':
            fla.setCheckable(True)
            fla.setChecked(True)
        else:
            pa.setCheckable(True)
            pa.setChecked(True)

        if config['sort_tags_by'] != "name":
            fla.setEnabled(False)
            m.hovered.connect(self.collapse_menu_hovered)
            fla.setToolTip(_('First letter is usable only when sorting by name'))
            # Apparently one cannot set a tooltip to empty, so use a star and
            # deal with it in the hover method
            da.setToolTip('*')
            pa.setToolTip('*')

        if index.isValid() and self.model().rowCount(index) > 0:
            self.context_menu.addSeparator()
            self.context_menu.addAction(_('E&xpand all children'), partial(self.expand_node_and_descendants, index))

        if not self.context_menu.isEmpty():
            self.context_menu.popup(self.mapToGlobal(point))
        return True

    def expand_node_and_descendants(self, index):
        if not index.isValid():
            return
        self.expand(index)
        for r in xrange(self.model().rowCount(index)):
            self.expand_node_and_descendants(index.child(r, 0))

    def collapse_menu_hovered(self, action):
        tip = action.toolTip()
        if tip == '*':
            tip = ''
        QToolTip.showText(QCursor.pos(), tip)

    def dragMoveEvent(self, event):
        QTreeView.dragMoveEvent(self, event)
        self.setDropIndicatorShown(False)
        index = self.indexAt(event.pos())
        if not index.isValid():
            return
        src_is_tb = event.mimeData().hasFormat('application/calibre+from_tag_browser')
        item = index.data(Qt.UserRole)
        if item.type == TagTreeItem.ROOT:
            return
        flags = self._model.flags(index)
        if item.type == TagTreeItem.TAG and flags & Qt.ItemIsDropEnabled:
            self.setDropIndicatorShown(not src_is_tb)
            return
        if item.type == TagTreeItem.CATEGORY and not item.is_gst:
            fm_dest = self.db.metadata_for_field(item.category_key)
            if fm_dest['kind'] == 'user':
                if src_is_tb:
                    if event.dropAction() == Qt.MoveAction:
                        data = str(event.mimeData().data('application/calibre+from_tag_browser'))
                        src = cPickle.loads(data)
                        for s in src:
                            if s[0] == TagTreeItem.TAG and \
                                    (not s[1].startswith('@') or s[2]):
                                return
                    self.setDropIndicatorShown(True)
                    return
                md = event.mimeData()
                if hasattr(md, 'column_name'):
                    fm_src = self.db.metadata_for_field(md.column_name)
                    if md.column_name in ['authors', 'publisher', 'series'] or \
                            (fm_src['is_custom'] and (
                             (fm_src['datatype'] in ['series', 'text', 'enumeration'] and
                              not fm_src['is_multiple']) or
                             (fm_src['datatype'] == 'composite' and
                              fm_src['display'].get('make_category', False)))):
                        self.setDropIndicatorShown(True)

    def clear(self):
        if self.model():
            self.model().clear_state()

    def is_visible(self, idx):
        item = idx.data(Qt.UserRole)
        if getattr(item, 'type', None) == TagTreeItem.TAG:
            idx = idx.parent()
        return self.isExpanded(idx)

    def recount(self, *args):
        '''
        Rebuild the category tree, expand any categories that were expanded,
        reset the search states, and reselect the current node.
        '''
        if self.disable_recounting or not self.pane_is_visible:
            return
        self.refresh_signal_processed = True
        ci = self.currentIndex()
        if not ci.isValid():
            ci = self.indexAt(QPoint(10, 10))
        path = self.model().path_for_index(ci) if self.is_visible(ci) else None
        expanded_categories, state_map = self.get_state()
        self._model.rebuild_node_tree(state_map=state_map)
        self.blockSignals(True)
        for category in expanded_categories:
            idx = self._model.index_for_category(category)
            if idx is not None and idx.isValid():
                self.expand(idx)
        self.show_item_at_path(path)
        self.blockSignals(False)

    def show_item_at_path(self, path, box=False,
                          position=QTreeView.PositionAtCenter):
        '''
        Scroll the browser and open categories to show the item referenced by
        path. If possible, the item is placed in the center. If box=True, a
        box is drawn around the item.
        '''
        if path:
            self.show_item_at_index(self._model.index_for_path(path), box=box,
                                    position=position)

    def expand_parent(self, idx):
        # Needed otherwise Qt sometimes segfaults if the node is buried in a
        # collapsed, off screen hierarchy. To be safe, we expand from the
        # outermost in
        p = self._model.parent(idx)
        if p.isValid():
            self.expand_parent(p)
        self.expand(idx)

    def show_item_at_index(self, idx, box=False,
                           position=QTreeView.PositionAtCenter):
        if idx.isValid() and idx.data(Qt.UserRole) is not self._model.root_item:
            self.expand_parent(idx)
            self.setCurrentIndex(idx)
            self.scrollTo(idx, position)
            if box:
                self._model.set_boxed(idx)

    def item_expanded(self, idx):
        '''
        Called by the expanded signal
        '''
        self.setCurrentIndex(idx)
예제 #32
0
class LocationManager(QObject):  # {{{

    locations_changed = pyqtSignal()
    unmount_device = pyqtSignal()
    location_selected = pyqtSignal(object)
    configure_device = pyqtSignal()
    update_device_metadata = pyqtSignal()

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.free = [-1, -1, -1]
        self.count = 0
        self.location_actions = QActionGroup(self)
        self.location_actions.setExclusive(True)
        self.current_location = 'library'
        self._mem = []
        self.tooltips = {}

        self.all_actions = []

        def ac(name, text, icon, tooltip):
            icon = QIcon(I(icon))
            ac = self.location_actions.addAction(icon, text)
            setattr(self, 'location_' + name, ac)
            ac.setAutoRepeat(False)
            ac.setCheckable(True)
            receiver = partial(self._location_selected, name)
            ac.triggered.connect(receiver)
            self.tooltips[name] = tooltip

            m = QMenu(parent)
            self._mem.append(m)
            a = m.addAction(icon, tooltip)
            a.triggered.connect(receiver)
            if name != 'library':
                self._mem.append(a)
                a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
                a.triggered.connect(self._eject_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('config.png')),
                                _('Configure this device'))
                a.triggered.connect(self._configure_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('sync.png')),
                                _('Update cached metadata on device'))
                a.triggered.connect(
                    lambda x: self.update_device_metadata.emit())
                self._mem.append(a)

            else:
                ac.setToolTip(tooltip)
            ac.setMenu(m)
            ac.calibre_name = name

            self.all_actions.append(ac)
            return ac

        self.library_action = ac('library', _('Library'), 'lt.png',
                                 _('Show books in calibre library'))
        ac('main', _('Device'), 'reader.png',
           _('Show books in the main memory of the device'))
        ac('carda', _('Card A'), 'sd.png', _('Show books in storage card A'))
        ac('cardb', _('Card B'), 'sd.png', _('Show books in storage card B'))

    def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
                           switch_actions, choose_action):
        self.switch_menu = self.library_action.menu()
        if self.switch_menu:
            self.switch_menu.addSeparator()
        else:
            self.switch_menu = QMenu()

        self.switch_menu.addAction(choose_action)
        self.cs_menus = []
        for t, acs in [(_('Quick switch'), quick_actions),
                       (_('Rename library'), rename_actions),
                       (_('Delete library'), delete_actions)]:
            if acs:
                self.cs_menus.append(QMenu(t))
                for ac in acs:
                    self.cs_menus[-1].addAction(ac)
                self.switch_menu.addMenu(self.cs_menus[-1])
        self.switch_menu.addSeparator()
        for ac in switch_actions:
            self.switch_menu.addAction(ac)

        if self.switch_menu != self.library_action.menu():
            self.library_action.setMenu(self.switch_menu)

    def _location_selected(self, location, *args):
        if location != self.current_location and hasattr(
                self, 'location_' + location):
            self.current_location = location
            self.location_selected.emit(location)
            getattr(self, 'location_' + location).setChecked(True)

    def _eject_requested(self, *args):
        self.unmount_device.emit()

    def _configure_requested(self):
        self.configure_device.emit()

    def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
        if icon is None:
            icon = I('reader.png')
        self.location_main.setIcon(QIcon(icon))
        had_device = self.has_device
        if cp is None:
            cp = (None, None)
        if isinstance(cp, (str, unicode)):
            cp = (cp, None)
        if len(fs) < 3:
            fs = list(fs) + [0]
        self.free[0] = fs[0]
        self.free[1] = fs[1]
        self.free[2] = fs[2]
        cpa, cpb = cp
        self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1
        self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
        self.update_tooltips()
        if self.has_device != had_device:
            self.location_library.setChecked(True)
            self.locations_changed.emit()
            if not self.has_device:
                self.location_library.trigger()

    def update_tooltips(self):
        for i, loc in enumerate(('main', 'carda', 'cardb')):
            t = self.tooltips[loc]
            if self.free[i] > -1:
                t += u'\n\n%s ' % human_readable(self.free[i]) + _('available')
            ac = getattr(self, 'location_' + loc)
            ac.setToolTip(t)
            ac.setWhatsThis(t)
            ac.setStatusTip(t)

    @property
    def has_device(self):
        return max(self.free) > -1

    @property
    def available_actions(self):
        ans = [self.location_library]
        for i, loc in enumerate(('main', 'carda', 'cardb')):
            if self.free[i] > -1:
                ans.append(getattr(self, 'location_' + loc))
        return ans
예제 #33
0
파일: layout.py 프로젝트: sj660/litebrary
class LocationManager(QObject):  # {{{

    locations_changed = pyqtSignal()
    unmount_device = pyqtSignal()
    location_selected = pyqtSignal(object)
    configure_device = pyqtSignal()
    update_device_metadata = pyqtSignal()

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.free = [-1, -1, -1]
        self.count = 0
        self.location_actions = QActionGroup(self)
        self.location_actions.setExclusive(True)
        self.current_location = 'library'
        self._mem = []
        self.tooltips = {}

        self.all_actions = []

        def ac(name, text, icon, tooltip):
            icon = QIcon(I(icon))
            ac = self.location_actions.addAction(icon, text)
            setattr(self, 'location_'+name, ac)
            ac.setAutoRepeat(False)
            ac.setCheckable(True)
            receiver = partial(self._location_selected, name)
            ac.triggered.connect(receiver)
            self.tooltips[name] = tooltip

            m = QMenu(parent)
            self._mem.append(m)
            a = m.addAction(icon, tooltip)
            a.triggered.connect(receiver)
            if name != 'library':
                self._mem.append(a)
                a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
                a.triggered.connect(self._eject_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('config.png')), _('Configure this device'))
                a.triggered.connect(self._configure_requested)
                self._mem.append(a)
                a = m.addAction(QIcon(I('sync.png')), _('Update cached metadata on device'))
                a.triggered.connect(lambda x : self.update_device_metadata.emit())
                self._mem.append(a)

            else:
                ac.setToolTip(tooltip)
            ac.setMenu(m)
            ac.calibre_name = name

            self.all_actions.append(ac)
            return ac

        self.library_action = ac('library', _('Library'), 'lt.png',
                _('Show books in calibre library'))
        ac('main', _('Device'), 'reader.png',
                _('Show books in the main memory of the device'))
        ac('carda', _('Card A'), 'sd.png',
                _('Show books in storage card A'))
        ac('cardb', _('Card B'), 'sd.png',
                _('Show books in storage card B'))

    def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
            switch_actions, choose_action):
        self.switch_menu = self.library_action.menu()
        if self.switch_menu:
            self.switch_menu.addSeparator()
        else:
            self.switch_menu = QMenu()

        self.switch_menu.addAction(choose_action)
        self.cs_menus = []
        for t, acs in [(_('Quick switch'), quick_actions),
                (_('Rename library'), rename_actions),
                (_('Delete library'), delete_actions)]:
            if acs:
                self.cs_menus.append(QMenu(t))
                for ac in acs:
                    self.cs_menus[-1].addAction(ac)
                self.switch_menu.addMenu(self.cs_menus[-1])
        self.switch_menu.addSeparator()
        for ac in switch_actions:
            self.switch_menu.addAction(ac)

        if self.switch_menu != self.library_action.menu():
            self.library_action.setMenu(self.switch_menu)

    def _location_selected(self, location, *args):
        if location != self.current_location and hasattr(self,
                'location_'+location):
            self.current_location = location
            self.location_selected.emit(location)
            getattr(self, 'location_'+location).setChecked(True)

    def _eject_requested(self, *args):
        self.unmount_device.emit()

    def _configure_requested(self):
        self.configure_device.emit()

    def update_devices(self, cp=(None, None), fs=[-1, -1, -1], icon=None):
        if icon is None:
            icon = I('reader.png')
        self.location_main.setIcon(QIcon(icon))
        had_device = self.has_device
        if cp is None:
            cp = (None, None)
        if isinstance(cp, (str, unicode)):
            cp = (cp, None)
        if len(fs) < 3:
            fs = list(fs) + [0]
        self.free[0] = fs[0]
        self.free[1] = fs[1]
        self.free[2] = fs[2]
        cpa, cpb = cp
        self.free[1] = fs[1] if fs[1] is not None and cpa is not None else -1
        self.free[2] = fs[2] if fs[2] is not None and cpb is not None else -1
        self.update_tooltips()
        if self.has_device != had_device:
            self.location_library.setChecked(True)
            self.locations_changed.emit()
            if not self.has_device:
                self.location_library.trigger()

    def update_tooltips(self):
        for i, loc in enumerate(('main', 'carda', 'cardb')):
            t = self.tooltips[loc]
            if self.free[i] > -1:
                t += u'\n\n%s '%human_readable(self.free[i]) + _('available')
            ac = getattr(self, 'location_'+loc)
            ac.setToolTip(t)
            ac.setWhatsThis(t)
            ac.setStatusTip(t)

    @property
    def has_device(self):
        return max(self.free) > -1

    @property
    def available_actions(self):
        ans = [self.location_library]
        for i, loc in enumerate(('main', 'carda', 'cardb')):
            if self.free[i] > -1:
                ans.append(getattr(self, 'location_'+loc))
        return ans
예제 #34
0
파일: widget.py 프로젝트: davidfor/calibre
    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction
        c = self.editor.cursorForPosition(pos)
        origc = QTextCursor(c)
        current_cursor = self.editor.textCursor()
        r = origr = self.editor.syntax_range_for_cursor(c)
        if (r is None or not r.format.property(SPELL_PROPERTY)) and c.positionInBlock() > 0 and not current_cursor.hasSelection():
            c.setPosition(c.position() - 1)
            r = self.editor.syntax_range_for_cursor(c)

        if r is not None and r.format.property(SPELL_PROPERTY):
            word = self.editor.text_for_range(c.block(), r)
            locale = self.editor.spellcheck_locale_for_cursor(c)
            orig_pos = c.position()
            c.setPosition(orig_pos - utf16_length(word))
            found = False
            self.editor.setTextCursor(c)
            if self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False):
                found = True
                fc = self.editor.textCursor()
                if fc.position() < c.position():
                    self.editor.find_spell_word([word], locale.langcode, center_on_cursor=False)
            spell_cursor = self.editor.textCursor()
            if current_cursor.hasSelection():
                # Restore the current cursor so that any selection is preserved
                # for the change case actions
                self.editor.setTextCursor(current_cursor)
            if found:
                suggestions = dictionaries.suggestions(word, locale)[:7]
                if suggestions:
                    for suggestion in suggestions:
                        ac = m.addAction(suggestion, partial(self.editor.simple_replace, suggestion, cursor=spell_cursor))
                        f = ac.font()
                        f.setBold(True), ac.setFont(f)
                    m.addSeparator()
                m.addAction(actions['spell-next'])
                m.addAction(_('Ignore this word'), partial(self._nuke_word, None, word, locale))
                dics = dictionaries.active_user_dictionaries
                if len(dics) > 0:
                    if len(dics) == 1:
                        m.addAction(_('Add this word to the dictionary: {0}').format(dics[0].name), partial(
                            self._nuke_word, dics[0].name, word, locale))
                    else:
                        ac = m.addAction(_('Add this word to the dictionary'))
                        dmenu = QMenu(m)
                        ac.setMenu(dmenu)
                        for dic in dics:
                            dmenu.addAction(dic.name, partial(self._nuke_word, dic.name, word, locale))
                m.addSeparator()

        if origr is not None and origr.format.property(LINK_PROPERTY):
            href = self.editor.text_for_range(origc.block(), origr)
            m.addAction(_('Open %s') % href, partial(self.link_clicked.emit, href))

        if origr is not None and (origr.format.property(TAG_NAME_PROPERTY) or origr.format.property(CSS_PROPERTY)):
            word = self.editor.text_for_range(origc.block(), origr)
            item_type = 'tag_name' if origr.format.property(TAG_NAME_PROPERTY) else 'css_property'
            url = help_url(word, item_type, self.editor.highlighter.doc_name, extra_data=current_container().opf_version)
            if url is not None:
                m.addAction(_('Show help for: %s') % word, partial(open_url, url))

        for x in ('undo', 'redo'):
            ac = actions['editor-%s' % x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        for x in ('cut', 'copy', 'paste'):
            ac = actions['editor-' + x]
            if ac.isEnabled():
                a(ac)
        m.addSeparator()
        m.addAction(_('&Select all'), self.editor.select_all)
        if self.selected_text or self.has_marked_text:
            update_mark_text_action(self)
            m.addAction(actions['mark-selected-text'])
        if self.syntax != 'css' and actions['editor-cut'].isEnabled():
            cm = QMenu(_('Change &case'), m)
            for ac in 'upper lower swap title capitalize'.split():
                cm.addAction(actions['transform-case-' + ac])
            m.addMenu(cm)
        if self.syntax == 'html':
            m.addAction(actions['multisplit'])
        m.exec_(self.editor.viewport().mapToGlobal(pos))
예제 #35
0
파일: main.py 프로젝트: nospy/calibre
    def setup_ui(self):
        self.setWindowIcon(QIcon(I('diff.png')))
        self.stacks = st = QStackedLayout(self)
        self.busy = BusyWidget(self)
        self.w = QWidget(self)
        st.addWidget(self.busy), st.addWidget(self.w)

        self.setLayout(st)
        self.l = l = QGridLayout()
        self.w.setLayout(l)

        self.view = v = DiffView(self, show_open_in_editor=self.show_open_in_editor)
        l.addWidget(v, l.rowCount(), 0, 1, -1)

        r = l.rowCount()
        self.bp = b = QToolButton(self)
        b.setIcon(QIcon(I('back.png')))
        b.clicked.connect(partial(self.view.next_change, -1))
        b.setToolTip(_('Go to previous change') + ' [p]')
        b.setText(_('&Previous change')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 0)

        self.bn = b = QToolButton(self)
        b.setIcon(QIcon(I('forward.png')))
        b.clicked.connect(partial(self.view.next_change, 1))
        b.setToolTip(_('Go to next change') + ' [n]')
        b.setText(_('&Next change')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 1)

        self.search = s = HistoryLineEdit2(self)
        s.initialize('diff_search_history')
        l.addWidget(s, r, 2)
        s.setPlaceholderText(_('Search for text'))
        s.returnPressed.connect(partial(self.do_search, False))
        self.sbn = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-down.png')))
        b.clicked.connect(partial(self.do_search, False))
        b.setToolTip(_('Find next match'))
        b.setText(_('Next &match')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 3)
        self.sbp = b = QToolButton(self)
        b.setIcon(QIcon(I('arrow-up.png')))
        b.clicked.connect(partial(self.do_search, True))
        b.setToolTip(_('Find previous match'))
        b.setText(_('P&revious match')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        l.addWidget(b, r, 4)
        self.lb = b = QRadioButton(_('Left panel'), self)
        b.setToolTip(_('Perform search in the left panel'))
        l.addWidget(b, r, 5)
        self.rb = b = QRadioButton(_('Right panel'), self)
        b.setToolTip(_('Perform search in the right panel'))
        l.addWidget(b, r, 6)
        b.setChecked(True)
        self.pb = b = QToolButton(self)
        b.setIcon(QIcon(I('config.png')))
        b.setText(_('&Options')), b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        b.setToolTip(_('Change how the differences are displayed'))
        b.setPopupMode(b.InstantPopup)
        m = QMenu(b)
        b.setMenu(m)
        cm = self.cm = QMenu(_('Lines of context around each change'))
        for i in (3, 5, 10, 50):
            cm.addAction(_('Show %d lines of context') % i, partial(self.change_context, i))
        cm.addAction(_('Show all text'), partial(self.change_context, None))
        self.beautify_action = m.addAction('', self.toggle_beautify)
        self.set_beautify_action_text()
        m.addMenu(cm)
        l.addWidget(b, r, 7)

        self.hl = QHBoxLayout()
        l.addLayout(self.hl, l.rowCount(), 0, 1, -1)
        self.names = QLabel('')
        self.hl.addWidget(self.names, r)

        self.bb.setStandardButtons(self.bb.Close)
        if self.revert_button_msg is not None:
            self.rvb = b = self.bb.addButton(self.revert_button_msg, self.bb.ActionRole)
            b.setIcon(QIcon(I('edit-undo.png'))), b.setAutoDefault(False)
            b.clicked.connect(self.revert_requested)
            b.clicked.connect(self.reject)
        self.bb.button(self.bb.Close).setDefault(True)
        self.hl.addWidget(self.bb, r)

        self.view.setFocus(Qt.OtherFocusReason)
예제 #36
0
파일: views.py 프로젝트: newnone/calibre
class BooksView(QTableView):  # {{{

    files_dropped = pyqtSignal(object)
    add_column_signal = pyqtSignal()
    is_library_view = True

    def viewportEvent(self, event):
        if (event.type() == event.ToolTip and not gprefs['book_list_tooltips']):
            return False
        return QTableView.viewportEvent(self, event)

    def __init__(self, parent, modelcls=BooksModel, use_edit_metadata_dialog=True):
        QTableView.__init__(self, parent)
        self.default_row_height = self.verticalHeader().defaultSectionSize()
        self.gui = parent
        self.setProperty('highlight_current_item', 150)
        self.row_sizing_done = False
        self.alternate_views = AlternateViews(self)

        if not tweaks['horizontal_scrolling_per_column']:
            self.setHorizontalScrollMode(self.ScrollPerPixel)

        self.setEditTriggers(self.EditKeyPressed)
        if tweaks['doubleclick_on_library_view'] == 'edit_cell':
            self.setEditTriggers(self.DoubleClicked|self.editTriggers())
        elif tweaks['doubleclick_on_library_view'] == 'open_viewer':
            self.setEditTriggers(self.SelectedClicked|self.editTriggers())
            self.doubleClicked.connect(parent.iactions['View'].view_triggered)
        elif tweaks['doubleclick_on_library_view'] == 'edit_metadata':
            # Must not enable single-click to edit, or the field will remain
            # open in edit mode underneath the edit metadata dialog
            if use_edit_metadata_dialog:
                self.doubleClicked.connect(
                        partial(parent.iactions['Edit Metadata'].edit_metadata,
                                checked=False))
            else:
                self.setEditTriggers(self.DoubleClicked|self.editTriggers())

        setup_dnd_interface(self)
        self.setAlternatingRowColors(True)
        self.setShowGrid(False)
        self.setWordWrap(False)

        self.rating_delegate = RatingDelegate(self)
        self.timestamp_delegate = DateDelegate(self)
        self.pubdate_delegate = PubDateDelegate(self)
        self.last_modified_delegate = DateDelegate(self,
                tweak_name='gui_last_modified_display_format')
        self.languages_delegate = LanguagesDelegate(self)
        self.tags_delegate = CompleteDelegate(self, ',', 'all_tag_names')
        self.authors_delegate = CompleteDelegate(self, '&', 'all_author_names', True)
        self.cc_names_delegate = CompleteDelegate(self, '&', 'all_custom', True)
        self.series_delegate = TextDelegate(self)
        self.publisher_delegate = TextDelegate(self)
        self.text_delegate = TextDelegate(self)
        self.cc_text_delegate = CcTextDelegate(self)
        self.cc_enum_delegate = CcEnumDelegate(self)
        self.cc_bool_delegate = CcBoolDelegate(self)
        self.cc_comments_delegate = CcCommentsDelegate(self)
        self.cc_template_delegate = CcTemplateDelegate(self)
        self.cc_number_delegate = CcNumberDelegate(self)
        self.display_parent = parent
        self._model = modelcls(self)
        self.setModel(self._model)
        self._model.count_changed_signal.connect(self.do_row_sizing,
                                                 type=Qt.QueuedConnection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setSortingEnabled(True)
        self.selectionModel().currentRowChanged.connect(self._model.current_changed)
        self.preserve_state = partial(PreserveViewState, self)
        self.marked_changed_listener = FunctionDispatcher(self.marked_changed)

        # {{{ Column Header setup
        self.can_add_columns = True
        self.was_restored = False
        self.column_header = HeaderView(Qt.Horizontal, self)
        self.setHorizontalHeader(self.column_header)
        self.column_header.sortIndicatorChanged.disconnect()
        self.column_header.sortIndicatorChanged.connect(self.user_sort_requested)
        self.column_header.setSectionsMovable(True)
        self.column_header.setSectionsClickable(True)
        self.column_header.sectionMoved.connect(self.save_state)
        self.column_header.setContextMenuPolicy(Qt.CustomContextMenu)
        self.column_header.customContextMenuRequested.connect(self.show_column_header_context_menu)
        self.column_header.sectionResized.connect(self.column_resized, Qt.QueuedConnection)
        self.row_header = HeaderView(Qt.Vertical, self)
        self.row_header.setSectionResizeMode(self.row_header.Fixed)
        self.setVerticalHeader(self.row_header)
        # }}}

        self._model.database_changed.connect(self.database_changed)
        hv = self.verticalHeader()
        hv.setSectionsClickable(True)
        hv.setCursor(Qt.PointingHandCursor)
        self.selected_ids = []
        self._model.about_to_be_sorted.connect(self.about_to_be_sorted)
        self._model.sorting_done.connect(self.sorting_done,
                type=Qt.QueuedConnection)

    # Column Header Context Menu {{{
    def column_header_context_handler(self, action=None, column=None):
        if not action or not column:
            return
        try:
            idx = self.column_map.index(column)
        except:
            return
        h = self.column_header

        if action == 'hide':
            if h.hiddenSectionCount() >= h.count():
                return error_dialog(self, _('Cannot hide all columns'), _(
                    'You must not hide all columns'), show=True)
            h.setSectionHidden(idx, True)
        elif action == 'show':
            h.setSectionHidden(idx, False)
            if h.sectionSize(idx) < 3:
                sz = h.sectionSizeHint(idx)
                h.resizeSection(idx, sz)
        elif action == 'ascending':
            self.sort_by_column_and_order(idx, True)
        elif action == 'descending':
            self.sort_by_column_and_order(idx, False)
        elif action == 'defaults':
            self.apply_state(self.get_default_state())
        elif action == 'addcustcol':
            self.add_column_signal.emit()
        elif action.startswith('align_'):
            alignment = action.partition('_')[-1]
            self._model.change_alignment(column, alignment)
        elif action == 'quickview':
            from calibre.customize.ui import find_plugin
            qv = find_plugin('Show Quickview')
            if qv:
                rows = self.selectionModel().selectedRows()
                if len(rows) > 0:
                    current_row = rows[0].row()
                    current_col = self.column_map.index(column)
                    index = self.model().index(current_row, current_col)
                    qv.actual_plugin_.change_quickview_column(index)

        self.save_state()

    def show_column_header_context_menu(self, pos):
        idx = self.column_header.logicalIndexAt(pos)
        if idx > -1 and idx < len(self.column_map):
            col = self.column_map[idx]
            name = unicode(self.model().headerData(idx, Qt.Horizontal,
                    Qt.DisplayRole) or '')
            self.column_header_context_menu = QMenu(self)
            if col != 'ondevice':
                self.column_header_context_menu.addAction(_('Hide column %s') %
                        name,
                    partial(self.column_header_context_handler, action='hide',
                        column=col))
            m = self.column_header_context_menu.addMenu(
                    _('Sort on %s')  % name)
            a = m.addAction(_('Ascending'),
                    partial(self.column_header_context_handler,
                        action='ascending', column=col))
            d = m.addAction(_('Descending'),
                    partial(self.column_header_context_handler,
                        action='descending', column=col))
            if self._model.sorted_on[0] == col:
                ac = a if self._model.sorted_on[1] else d
                ac.setCheckable(True)
                ac.setChecked(True)
            if col not in ('ondevice', 'inlibrary') and \
                    (not self.model().is_custom_column(col) or
                    self.model().custom_columns[col]['datatype'] not in ('bool',
                        )):
                m = self.column_header_context_menu.addMenu(
                        _('Change text alignment for %s') % name)
                al = self._model.alignment_map.get(col, 'left')
                for x, t in (('left', _('Left')), ('right', _('Right')), ('center',
                    _('Center'))):
                        a = m.addAction(t,
                            partial(self.column_header_context_handler,
                            action='align_'+x, column=col))
                        if al == x:
                            a.setCheckable(True)
                            a.setChecked(True)

            if not isinstance(self, DeviceBooksView):
                if self._model.db.field_metadata[col]['is_category']:
                    act = self.column_header_context_menu.addAction(_('Quickview column %s') %
                            name,
                        partial(self.column_header_context_handler, action='quickview',
                            column=col))
                    rows = self.selectionModel().selectedRows()
                    if len(rows) > 1:
                        act.setEnabled(False)

            hidden_cols = [self.column_map[i] for i in
                    range(self.column_header.count()) if
                    self.column_header.isSectionHidden(i)]
            try:
                hidden_cols.remove('ondevice')
            except:
                pass
            if hidden_cols:
                self.column_header_context_menu.addSeparator()
                m = self.column_header_context_menu.addMenu(_('Show column'))
                for col in hidden_cols:
                    hidx = self.column_map.index(col)
                    name = unicode(self.model().headerData(hidx, Qt.Horizontal,
                            Qt.DisplayRole) or '')
                    m.addAction(name,
                        partial(self.column_header_context_handler,
                        action='show', column=col))

            self.column_header_context_menu.addSeparator()
            self.column_header_context_menu.addAction(
                    _('Shrink column if it is too wide to fit'),
                    partial(self.resize_column_to_fit, column=self.column_map[idx]))
            self.column_header_context_menu.addAction(
                    _('Restore default layout'),
                    partial(self.column_header_context_handler,
                        action='defaults', column=col))

            if self.can_add_columns:
                self.column_header_context_menu.addAction(
                        QIcon(I('column.png')),
                        _('Add your own columns'),
                        partial(self.column_header_context_handler,
                            action='addcustcol', column=col))

            self.column_header_context_menu.popup(self.column_header.mapToGlobal(pos))
    # }}}

    # Sorting {{{
    def sort_by_column_and_order(self, col, ascending):
        self.column_header.blockSignals(True)
        self.sortByColumn(col, Qt.AscendingOrder if ascending else Qt.DescendingOrder)
        self.column_header.blockSignals(False)

    def user_sort_requested(self, col, order=Qt.AscendingOrder):
        if col >= len(self.column_map) or col < 0:
            return QTableView.sortByColumn(self, col)
        field = self.column_map[col]
        self.intelligent_sort(field, order == Qt.AscendingOrder)

    def intelligent_sort(self, field, ascending):
        m = self.model()
        pname = 'previous_sort_order_' + self.__class__.__name__
        previous = gprefs.get(pname, {})
        if field == m.sorted_on[0] or field not in previous:
            self.sort_by_named_field(field, ascending)
            previous[field] = ascending
            gprefs[pname] = previous
            return
        previous[m.sorted_on[0]] = m.sorted_on[1]
        gprefs[pname] = previous
        self.sort_by_named_field(field, previous[field])

    def about_to_be_sorted(self, idc):
        selected_rows = [r.row() for r in self.selectionModel().selectedRows()]
        self.selected_ids = [idc(r) for r in selected_rows]

    def sorting_done(self, indexc):
        pos = self.horizontalScrollBar().value()
        self.select_rows(self.selected_ids, using_ids=True, change_current=True,
            scroll=True)
        self.selected_ids = []
        self.horizontalScrollBar().setValue(pos)

    def sort_by_named_field(self, field, order, reset=True):
        if field in self.column_map:
            idx = self.column_map.index(field)
            self.sort_by_column_and_order(idx, order)
        else:
            self._model.sort_by_named_field(field, order, reset)
            self.column_header.blockSignals(True)
            self.column_header.setSortIndicator(-1, Qt.AscendingOrder)
            self.column_header.blockSignals(False)

    def multisort(self, fields, reset=True, only_if_different=False):
        if len(fields) == 0:
            return
        sh = self.cleanup_sort_history(self._model.sort_history,
                                       ignore_column_map=True)
        if only_if_different and len(sh) >= len(fields):
            ret=True
            for i,t in enumerate(fields):
                if t[0] != sh[i][0]:
                    ret = False
                    break
            if ret:
                return

        for n,d in reversed(fields):
            if n in self._model.db.field_metadata.keys():
                sh.insert(0, (n, d))
        sh = self.cleanup_sort_history(sh, ignore_column_map=True)
        self._model.sort_history = [tuple(x) for x in sh]
        self._model.resort(reset=reset)
        col = fields[0][0]
        dir = Qt.AscendingOrder if fields[0][1] else Qt.DescendingOrder
        if col in self.column_map:
            col = self.column_map.index(col)
            self.column_header.blockSignals(True)
            try:
                self.column_header.setSortIndicator(col, dir)
            finally:
                self.column_header.blockSignals(False)
    # }}}

    # Ondevice column {{{
    def set_ondevice_column_visibility(self):
        col, h = self._model.column_map.index('ondevice'), self.column_header
        h.setSectionHidden(col, not self._model.device_connected)

    def set_device_connected(self, is_connected):
        self._model.set_device_connected(is_connected)
        self.set_ondevice_column_visibility()
    # }}}

    # Save/Restore State {{{
    def get_state(self):
        h = self.column_header
        cm = self.column_map
        state = {}
        state['hidden_columns'] = [cm[i] for i in range(h.count())
                if h.isSectionHidden(i) and cm[i] != 'ondevice']
        state['last_modified_injected'] = True
        state['languages_injected'] = True
        state['sort_history'] = \
            self.cleanup_sort_history(self.model().sort_history, ignore_column_map=self.is_library_view)
        state['column_positions'] = {}
        state['column_sizes'] = {}
        state['column_alignment'] = self._model.alignment_map
        for i in range(h.count()):
            name = cm[i]
            state['column_positions'][name] = h.visualIndex(i)
            if name != 'ondevice':
                state['column_sizes'][name] = h.sectionSize(i)
        return state

    def write_state(self, state):
        db = getattr(self.model(), 'db', None)
        name = unicode(self.objectName())
        if name and db is not None:
            db.new_api.set_pref(name + ' books view state', state)

    def save_state(self):
        # Only save if we have been initialized (set_database called)
        if len(self.column_map) > 0 and self.was_restored:
            state = self.get_state()
            self.write_state(state)

    def cleanup_sort_history(self, sort_history, ignore_column_map=False):
        history = []

        for col, order in sort_history:
            if not isinstance(order, bool):
                continue
            col = {'date':'timestamp', 'sort':'title'}.get(col, col)
            if ignore_column_map or col in self.column_map:
                if (not history or history[-1][0] != col):
                    history.append([col, order])
        return history

    def apply_sort_history(self, saved_history, max_sort_levels=3):
        if not saved_history:
            return
        if self.is_library_view:
            for col, order in reversed(self.cleanup_sort_history(
                    saved_history, ignore_column_map=True)[:max_sort_levels]):
                self.sort_by_named_field(col, order)
        else:
            for col, order in reversed(self.cleanup_sort_history(
                    saved_history)[:max_sort_levels]):
                self.sort_by_column_and_order(self.column_map.index(col), order)

    def apply_state(self, state, max_sort_levels=3):
        h = self.column_header
        cmap = {}
        hidden = state.get('hidden_columns', [])
        for i, c in enumerate(self.column_map):
            cmap[c] = i
            if c != 'ondevice':
                h.setSectionHidden(i, c in hidden)

        positions = state.get('column_positions', {})
        pmap = {}
        for col, pos in positions.items():
            if col in cmap:
                pmap[pos] = col
        for pos in sorted(pmap.keys()):
            col = pmap[pos]
            idx = cmap[col]
            current_pos = h.visualIndex(idx)
            if current_pos != pos:
                h.moveSection(current_pos, pos)

        # Because of a bug in Qt 5 we have to ensure that the header is actually
        # relaid out by changing this value, without this sometimes ghost
        # columns remain visible when changing libraries
        for i in xrange(h.count()):
            val = h.isSectionHidden(i)
            h.setSectionHidden(i, not val)
            h.setSectionHidden(i, val)

        sizes = state.get('column_sizes', {})
        for col, size in sizes.items():
            if col in cmap:
                sz = sizes[col]
                if sz < 3:
                    sz = h.sectionSizeHint(cmap[col])
                h.resizeSection(cmap[col], sz)

        self.apply_sort_history(state.get('sort_history', None),
                max_sort_levels=max_sort_levels)

        for col, alignment in state.get('column_alignment', {}).items():
            self._model.change_alignment(col, alignment)

        for i in range(h.count()):
            if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
                sz = h.sectionSizeHint(i)
                h.resizeSection(i, sz)

    def get_default_state(self):
        old_state = {
                'hidden_columns': ['last_modified', 'languages'],
                'sort_history':[DEFAULT_SORT],
                'column_positions': {},
                'column_sizes': {},
                'column_alignment': {
                    'size':'center',
                    'timestamp':'center',
                    'pubdate':'center'},
                'last_modified_injected': True,
                'languages_injected': True,
                }
        h = self.column_header
        cm = self.column_map
        for i in range(h.count()):
            name = cm[i]
            old_state['column_positions'][name] = i
            if name != 'ondevice':
                old_state['column_sizes'][name] = \
                    min(350, max(self.sizeHintForColumn(i),
                        h.sectionSizeHint(i)))
                if name in ('timestamp', 'last_modified'):
                    old_state['column_sizes'][name] += 12
        return old_state

    def get_old_state(self):
        ans = None
        name = unicode(self.objectName())
        if name:
            name += ' books view state'
            db = getattr(self.model(), 'db', None)
            if db is not None:
                ans = db.prefs.get(name, None)
                if ans is None:
                    ans = gprefs.get(name, None)
                    try:
                        del gprefs[name]
                    except:
                        pass
                    if ans is not None:
                        db.new_api.set_pref(name, ans)
                else:
                    injected = False
                    if not ans.get('last_modified_injected', False):
                        injected = True
                        ans['last_modified_injected'] = True
                        hc = ans.get('hidden_columns', [])
                        if 'last_modified' not in hc:
                            hc.append('last_modified')
                    if not ans.get('languages_injected', False):
                        injected = True
                        ans['languages_injected'] = True
                        hc = ans.get('hidden_columns', [])
                        if 'languages' not in hc:
                            hc.append('languages')
                    if injected:
                        db.new_api.set_pref(name, ans)
        return ans

    def restore_state(self):
        old_state = self.get_old_state()
        if old_state is None:
            old_state = self.get_default_state()
        max_levels = 3

        if tweaks['sort_columns_at_startup'] is not None:
            sh = []
            try:
                for c,d in tweaks['sort_columns_at_startup']:
                    if not isinstance(d, bool):
                        d = True if d == 0 else False
                    sh.append((c, d))
            except:
                # Ignore invalid tweak values as users seem to often get them
                # wrong
                print('Ignoring invalid sort_columns_at_startup tweak, with error:')
                import traceback
                traceback.print_exc()
            old_state['sort_history'] = sh
            max_levels = max(3, len(sh))

        self.column_header.blockSignals(True)
        self.apply_state(old_state, max_sort_levels=max_levels)
        self.column_header.blockSignals(False)

        self.do_row_sizing()

        self.was_restored = True

    def refresh_row_sizing(self):
        self.row_sizing_done = False
        self.do_row_sizing()

    def do_row_sizing(self):
        # Resize all rows to have the correct height
        if not self.row_sizing_done and self.model().rowCount(QModelIndex()) > 0:
            vh = self.verticalHeader()
            vh.setDefaultSectionSize(max(vh.minimumSectionSize(), self.default_row_height + gprefs['book_list_extra_row_spacing']))
            self._model.set_row_height(self.rowHeight(0))
            self.row_sizing_done = True

    def resize_column_to_fit(self, column):
        col = self.column_map.index(column)
        self.column_resized(col, self.columnWidth(col), self.columnWidth(col))

    def column_resized(self, col, old_size, new_size):
        # arbitrary: scroll bar + header + some
        max_width = self.width() - (self.verticalScrollBar().width() +
                                    self.verticalHeader().width() + 10)
        if max_width < 200:
            max_width = 200
        if new_size > max_width:
            self.column_header.blockSignals(True)
            self.setColumnWidth(col, max_width)
            self.column_header.blockSignals(False)

    # }}}

    # Initialization/Delegate Setup {{{

    def set_database(self, db):
        self.alternate_views.set_database(db)
        self.save_state()
        self._model.set_database(db)
        self.tags_delegate.set_database(db)
        self.cc_names_delegate.set_database(db)
        self.authors_delegate.set_database(db)
        self.series_delegate.set_auto_complete_function(db.all_series)
        self.publisher_delegate.set_auto_complete_function(db.all_publishers)
        self.alternate_views.set_database(db, stage=1)

    def marked_changed(self, old_marked, current_marked):
        self.alternate_views.marked_changed(old_marked, current_marked)
        if bool(old_marked) == bool(current_marked):
            changed = old_marked | current_marked
            i = self.model().db.data.id_to_index
            def f(x):
                try:
                    return i(x)
                except ValueError:
                    pass
            sections = tuple(x for x in map(f, changed) if x is not None)
            if sections:
                self.row_header.headerDataChanged(Qt.Vertical, min(sections), max(sections))
                # This is needed otherwise Qt does not always update the
                # viewport correctly. See https://bugs.launchpad.net/bugs/1404697
                self.row_header.viewport().update()
        else:
            # Marked items have either appeared or all been removed
            self.model().set_row_decoration(current_marked)
            self.row_header.headerDataChanged(Qt.Vertical, 0, self.row_header.count()-1)
            self.row_header.geometriesChanged.emit()

    def database_changed(self, db):
        db.data.add_marked_listener(self.marked_changed_listener)
        for i in range(self.model().columnCount(None)):
            if self.itemDelegateForColumn(i) in (self.rating_delegate,
                    self.timestamp_delegate, self.pubdate_delegate,
                    self.last_modified_delegate, self.languages_delegate):
                self.setItemDelegateForColumn(i, self.itemDelegate())

        cm = self.column_map

        for colhead in cm:
            if self._model.is_custom_column(colhead):
                cc = self._model.custom_columns[colhead]
                if cc['datatype'] == 'datetime':
                    delegate = CcDateDelegate(self)
                    delegate.set_format(cc['display'].get('date_format',''))
                    self.setItemDelegateForColumn(cm.index(colhead), delegate)
                elif cc['datatype'] == 'comments':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
                elif cc['datatype'] == 'text':
                    if cc['is_multiple']:
                        if cc['display'].get('is_names', False):
                            self.setItemDelegateForColumn(cm.index(colhead),
                                                          self.cc_names_delegate)
                        else:
                            self.setItemDelegateForColumn(cm.index(colhead),
                                                          self.tags_delegate)
                    else:
                        self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
                elif cc['datatype'] == 'series':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
                elif cc['datatype'] in ('int', 'float'):
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_number_delegate)
                elif cc['datatype'] == 'bool':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
                elif cc['datatype'] == 'rating':
                    self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
                elif cc['datatype'] == 'composite':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
                elif cc['datatype'] == 'enumeration':
                    self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate)
            else:
                dattr = colhead+'_delegate'
                delegate = colhead if hasattr(self, dattr) else 'text'
                self.setItemDelegateForColumn(cm.index(colhead), getattr(self,
                    delegate+'_delegate'))

        self.restore_state()
        self.set_ondevice_column_visibility()
        # incase there were marked books
        self.model().set_row_decoration(set())
        self.row_header.headerDataChanged(Qt.Vertical, 0, self.row_header.count()-1)
        self.row_header.geometriesChanged.emit()
        # }}}

    # Context Menu {{{
    def set_context_menu(self, menu, edit_collections_action):
        self.setContextMenuPolicy(Qt.DefaultContextMenu)
        self.context_menu = menu
        self.alternate_views.set_context_menu(menu)
        self.edit_collections_action = edit_collections_action

    def contextMenuEvent(self, event):
        sac = self.gui.iactions['Sort By']
        sort_added = tuple(ac for ac in self.context_menu.actions() if ac is sac.qaction)
        if sort_added:
            sac.update_menu()
        self.context_menu.popup(event.globalPos())
        event.accept()
    # }}}

    @property
    def column_map(self):
        return self._model.column_map

    @property
    def visible_columns(self):
        h = self.horizontalHeader()
        logical_indices = (x for x in xrange(h.count()) if not h.isSectionHidden(x))
        rmap = {i:x for i, x in enumerate(self.column_map)}
        return (rmap[h.visualIndex(x)] for x in logical_indices if h.visualIndex(x) > -1)

    def refresh_book_details(self):
        idx = self.currentIndex()
        if idx.isValid():
            self._model.current_changed(idx, idx)
            return True
        return False

    def scrollContentsBy(self, dx, dy):
        # Needed as Qt bug causes headerview to not always update when scrolling
        QTableView.scrollContentsBy(self, dx, dy)
        if dy != 0:
            self.column_header.update()

    def scroll_to_row(self, row):
        if row > -1:
            h = self.horizontalHeader()
            for i in range(h.count()):
                if not h.isSectionHidden(i) and h.sectionViewportPosition(i) >= 0:
                    self.scrollTo(self.model().index(row, i), self.PositionAtCenter)
                    break

    @property
    def current_book(self):
        ci = self.currentIndex()
        if ci.isValid():
            try:
                return self.model().db.data.index_to_id(ci.row())
            except (IndexError, ValueError, KeyError, TypeError, AttributeError):
                pass

    def current_book_state(self):
        return self.current_book, self.horizontalScrollBar().value()

    def restore_current_book_state(self, state):
        book_id, hpos = state
        try:
            row = self.model().db.data.id_to_index(book_id)
        except (IndexError, ValueError, KeyError, TypeError, AttributeError):
            return
        self.set_current_row(row)
        self.scroll_to_row(row)
        self.horizontalScrollBar().setValue(hpos)

    def set_current_row(self, row=0, select=True, for_sync=False):
        if row > -1 and row < self.model().rowCount(QModelIndex()):
            h = self.horizontalHeader()
            logical_indices = list(range(h.count()))
            logical_indices = [x for x in logical_indices if not
                    h.isSectionHidden(x)]
            pairs = [(x, h.visualIndex(x)) for x in logical_indices if
                    h.visualIndex(x) > -1]
            if not pairs:
                pairs = [(0, 0)]
            pairs.sort(cmp=lambda x,y:cmp(x[1], y[1]))
            i = pairs[0][0]
            index = self.model().index(row, i)
            if for_sync:
                sm = self.selectionModel()
                sm.setCurrentIndex(index, sm.NoUpdate)
            else:
                self.setCurrentIndex(index)
                if select:
                    sm = self.selectionModel()
                    sm.select(index, sm.ClearAndSelect|sm.Rows)

    def row_at_top(self):
        pos = 0
        while pos < 100:
            ans = self.rowAt(pos)
            if ans > -1:
                return ans
            pos += 5

    def row_at_bottom(self):
        pos = self.viewport().height()
        limit = pos - 100
        while pos > limit:
            ans = self.rowAt(pos)
            if ans > -1:
                return ans
            pos -= 5

    def moveCursor(self, action, modifiers):
        orig = self.currentIndex()
        index = QTableView.moveCursor(self, action, modifiers)
        if action == QTableView.MovePageDown:
            moved = index.row() - orig.row()
            try:
                rows = self.row_at_bottom() - self.row_at_top()
            except TypeError:
                rows = moved
            if moved > rows:
                index = self.model().index(orig.row() + rows, index.column())
        elif action == QTableView.MovePageUp:
            moved = orig.row() - index.row()
            try:
                rows = self.row_at_bottom() - self.row_at_top()
            except TypeError:
                rows = moved
            if moved > rows:
                index = self.model().index(orig.row() - rows, index.column())
        elif action == QTableView.MoveHome and modifiers & Qt.ControlModifier:
            return self.model().index(0, orig.column())
        elif action == QTableView.MoveEnd and modifiers & Qt.ControlModifier:
            return self.model().index(self.model().rowCount(QModelIndex()) - 1, orig.column())
        return index

    def selectionCommand(self, index, event):
        if event and event.type() == event.KeyPress and event.key() in (Qt.Key_Home, Qt.Key_End) and event.modifiers() & Qt.CTRL:
            return QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
        return super(BooksView, self).selectionCommand(index, event)

    def ids_to_rows(self, ids):
        row_map = OrderedDict()
        ids = frozenset(ids)
        m = self.model()
        for row in xrange(m.rowCount(QModelIndex())):
            if len(row_map) >= len(ids):
                break
            c = m.id(row)
            if c in ids:
                row_map[c] = row
        return row_map

    def select_rows(self, identifiers, using_ids=True, change_current=True,
            scroll=True):
        '''
        Select rows identified by identifiers. identifiers can be a set of ids,
        row numbers or QModelIndexes.
        '''
        rows = set([x.row() if hasattr(x, 'row') else x for x in
            identifiers])
        if using_ids:
            rows = set([])
            identifiers = set(identifiers)
            m = self.model()
            for row in xrange(m.rowCount(QModelIndex())):
                if m.id(row) in identifiers:
                    rows.add(row)
        rows = list(sorted(rows))
        if rows:
            row = rows[0]
            if change_current:
                self.set_current_row(row, select=False)
            if scroll:
                self.scroll_to_row(row)
        sm = self.selectionModel()
        sel = QItemSelection()
        m = self.model()
        max_col = m.columnCount(QModelIndex()) - 1
        # Create a range based selector for each set of contiguous rows
        # as supplying selectors for each individual row causes very poor
        # performance if a large number of rows has to be selected.
        for k, g in itertools.groupby(enumerate(rows), lambda (i,x):i-x):
            group = list(map(operator.itemgetter(1), g))
            sel.merge(QItemSelection(m.index(min(group), 0),
                m.index(max(group), max_col)), sm.Select)
        sm.select(sel, sm.ClearAndSelect)
예제 #37
0
    def __init__(self,
                 parent,
                 max_width=None,
                 max_height=None,
                 hide_on_close=False):
        QMainWindow.__init__(self, parent)
        self.signalShowMessage.connect(self.showMessage,
                                       type=Qt.QueuedConnection)
        self.signalShowErrorMessage.connect(self.showErrorMessage,
                                            type=Qt.QueuedConnection)
        self.setWindowIcon(pixmaps.tigger_starface.icon())
        self._currier = PersistentCurrier()
        self.hide()
        # init column constants
        for icol, col in enumerate(self.ViewModelColumns):
            setattr(self, "Column%s" % col.capitalize(), icol)
        # init GUI
        self.setWindowTitle("Tigger")
        self.setWindowIcon(QIcon(pixmaps.purr_logo.pm()))
        # central widget setup
        self.cw = QWidget(self)
        # The actual min width of the control dialog is ~396
        self._ctrl_dialog_min_size = 400  # approx value
        # The actual min width of the profile/zoom windows is ~256
        self._profile_and_zoom_widget_min_size = 300  # approx value
        # set usable screen space (90% of available)
        self.max_width = max_width
        self.max_height = max_height
        self.setCentralWidget(self.cw)
        cwlo = QVBoxLayout(self.cw)
        cwlo.setContentsMargins(5, 5, 5, 5)
        # make splitter
        spl1 = self._splitter1 = QSplitter(Qt.Vertical, self.cw)
        spl1.setOpaqueResize(False)
        cwlo.addWidget(spl1)
        # Create listview of LSM entries
        self.tw = SkyModelTreeWidget(spl1)
        self.tw.hide()

        # split bottom pane
        spl2 = self._splitter2 = QSplitter(Qt.Horizontal, spl1)
        spl2.setOpaqueResize(False)
        self._skyplot_stack = QWidget(spl2)
        self._skyplot_stack_lo = QVBoxLayout(self._skyplot_stack)
        self._skyplot_stack_lo.setContentsMargins(0, 0, 0, 0)

        # add plot
        self.skyplot = SkyModelPlotter(self._skyplot_stack, self)
        self.skyplot.resize(128, 128)
        self.skyplot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self._skyplot_stack_lo.addWidget(self.skyplot, 1000)
        self.skyplot.hide()
        self.skyplot.imagesChanged.connect(self._imagesChanged)
        self.skyplot.setupShowMessages(self.signalShowMessage)
        self.skyplot.setupShowErrorMessages(self.signalShowErrorMessage)

        self._grouptab_stack = QWidget(spl2)
        self._grouptab_stack_lo = lo = QVBoxLayout(self._grouptab_stack)
        self._grouptab_stack_lo.setContentsMargins(0, 0, 0, 0)
        # add groupings table
        self.grouptab = ModelGroupsTable(self._grouptab_stack)
        self.grouptab.setSizePolicy(QSizePolicy.Preferred,
                                    QSizePolicy.Preferred)
        self.hasSkyModel.connect(self.grouptab.setEnabled)
        lo.addWidget(self.grouptab, 1000)
        lo.addStretch(1)
        self.grouptab.hide()

        # add image controls -- parentless for now (setLayout will reparent them anyway)
        self.imgman = ImageManager()
        self.imgman.setMainWindow(self)
        self.imgman.setShowMessageSignal(self.signalShowMessage)
        self.imgman.setShowErrorMessageSignal(self.signalShowErrorMessage)
        self.skyplot.setImageManager(self.imgman)
        self.imgman.imagesChanged.connect(self._imagesChanged)

        # enable status line
        self.statusBar().show()
        # Create and populate main menu
        menubar = self.menuBar()
        # File menu
        file_menu = menubar.addMenu("&File")
        qa_open = file_menu.addAction("&Open model...", self._openFileCallback,
                                      Qt.CTRL + Qt.Key_O)
        qa_merge = file_menu.addAction("&Merge in model...",
                                       self._mergeFileCallback,
                                       Qt.CTRL + Qt.SHIFT + Qt.Key_O)
        self.hasSkyModel.connect(qa_merge.setEnabled)
        file_menu.addSeparator()
        qa_save = file_menu.addAction("&Save model", self.saveFile,
                                      Qt.CTRL + Qt.Key_S)
        self.isUpdated.connect(qa_save.setEnabled)
        qa_save_as = file_menu.addAction("Save model &as...", self.saveFileAs)
        self.hasSkyModel.connect(qa_save_as.setEnabled)
        qa_save_selection_as = file_menu.addAction("Save selection as...",
                                                   self.saveSelectionAs)
        self.hasSelection.connect(qa_save_selection_as.setEnabled)
        file_menu.addSeparator()
        qa_close = file_menu.addAction("&Close model", self.closeFile,
                                       Qt.CTRL + Qt.Key_W)
        self.hasSkyModel.connect(qa_close.setEnabled)
        qa_quit = file_menu.addAction("Quit", self.close, Qt.CTRL + Qt.Key_Q)

        # Image menu
        menubar.addMenu(self.imgman.getMenu())
        # Plot menu
        menubar.addMenu(self.skyplot.getMenu())

        # LSM Menu
        em = QMenu("&LSM", self)
        self._qa_em = menubar.addMenu(em)
        self._qa_em.setVisible(False)
        self.hasSkyModel.connect(self._qa_em.setVisible)
        self._column_view_menu = QMenu("&Show columns", self)
        self._qa_cv_menu = em.addMenu(self._column_view_menu)
        em.addSeparator()
        em.addAction("Select &all", self._selectAll, Qt.CTRL + Qt.Key_A)
        em.addAction("U&nselect all", self._unselectAll, Qt.CTRL + Qt.Key_N)
        em.addAction("&Invert selection", self._selectInvert,
                     Qt.CTRL + Qt.Key_I)
        em.addAction("Select b&y attribute...", self._showSourceSelector,
                     Qt.CTRL + Qt.Key_Y)
        em.addSeparator()
        qa_add_tag = em.addAction("&Tag selection...", self.addTagToSelection,
                                  Qt.CTRL + Qt.Key_T)
        self.hasSelection.connect(qa_add_tag.setEnabled)
        qa_del_tag = em.addAction("&Untag selection...",
                                  self.removeTagsFromSelection,
                                  Qt.CTRL + Qt.Key_U)
        self.hasSelection.connect(qa_del_tag.setEnabled)
        qa_del_sel = em.addAction("&Delete selection", self._deleteSelection)
        self.hasSelection.connect(qa_del_sel.setEnabled)

        # Tools menu
        tm = self._tools_menu = QMenu("&Tools", self)
        self._qa_tm = menubar.addMenu(tm)
        self._qa_tm.setVisible(False)
        self.hasSkyModel.connect(self._qa_tm.setVisible)

        # Help menu
        menubar.addSeparator()
        hm = self._help_menu = menubar.addMenu("&Help")
        hm.addAction("&About...", self._showAboutDialog)
        self._about_dialog = None

        # message handlers
        self.qerrmsg = QErrorMessage(self)

        # set initial state
        self.setAcceptDrops(True)
        self.model = None
        self.filename = None
        self._display_filename = None
        self._open_file_dialog = self._merge_file_dialog = self._save_as_dialog = self._save_sel_as_dialog = self._open_image_dialog = None
        self.isUpdated.emit(False)
        self.hasSkyModel.emit(False)
        self.hasSelection.emit(False)
        self._exiting = False

        # set initial layout
        self._current_layout = None
        self.setLayout(self.LayoutEmpty)
        dprint(1, "init complete")