def context_menu(self):
        popup_menu = HintedTextEdit.createStandardContextMenu(self)

        # Select the word under the cursor.
        cursor = self.textCursor()
        cursor.select(QTextCursor.WordUnderCursor)
        self.setTextCursor(cursor)

        # Check if the selected word is misspelled and offer spelling
        # suggestions if it is.
        spell_menu = None
        if self.textCursor().hasSelection():
            text = unicode(self.textCursor().selectedText())
            if not self.spellcheck.check(text):
                spell_menu = QMenu(N_('Spelling Suggestions'))
                for word in self.spellcheck.suggest(text):
                    action = SpellAction(word, spell_menu)
                    self.connect(action, SIGNAL('correct'), self.correct)
                    spell_menu.addAction(action)
                # Only add the spelling suggests to the menu if there are
                # suggestions.
                if len(spell_menu.actions()) > 0:
                    popup_menu.addSeparator()
                    popup_menu.addMenu(spell_menu)

        return popup_menu, spell_menu
Exemple #2
0
        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
Exemple #3
0
    def contextMenuEvent(self, event):
        popup_menu = self.createStandardContextMenu()
 
        # Select the word under the cursor.
        cursor = self.textCursor()
        cursor.select(QTextCursor.WordUnderCursor)
        self.setTextCursor(cursor)
 
        # Check if the selected word is misspelled and offer spelling
        # suggestions if it is.
        if self.textCursor().hasSelection() and self._active:
            text = unicode(self.textCursor().selectedText())
            if not self.dict.check(text):
                spell_menu = QMenu('Spelling Suggestions')
                for word in self.dict.suggest(text):
                    action = SpellAction(word, spell_menu)
                    action.correct.connect(self.correctWord)
                    spell_menu.addAction(action)
                # Only add the spelling suggests to the menu if there are
                # suggestions.
                if len(spell_menu.actions()) != 0:
                    popup_menu.insertSeparator(popup_menu.actions()[0])
                    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
 
        popup_menu.exec_(event.globalPos())
Exemple #4
0
 def showContextMenu(self, position):
     SuggestionContextMenu = self.ui.textEdit.createStandardContextMenu()
     cursor=self.ui.textEdit.textCursor()
     cursor.select(QTextCursor.WordUnderCursor)
     self.ui.textEdit.setTextCursor(cursor)
     if self.ui.textEdit.textCursor().hasSelection():
         a=self.ui.textEdit.textCursor().selectedText()
         if a[-1]==u'।':
             a=a.remove(u'।')
             cursor.movePosition(QTextCursor.Left,QTextCursor.KeepAnchor)
             self.ui.textEdit.setTextCursor(cursor)
         text = unicode(a).encode('utf-8')
         if not self.checker.spell(text):
             ignoreAction=QAction('Ignore',SuggestionContextMenu)
             ignoreAction.triggered.connect(self.ignoreSelection)
             addAction=QAction('Add to Dictionary',SuggestionContextMenu)
             addAction.triggered.connect(self.addWord)
             SuggestionContextMenu.insertSeparator(SuggestionContextMenu.actions()[0])
             SuggestionContextMenu.insertAction(SuggestionContextMenu.actions()[0], addAction)
             SuggestionContextMenu.insertAction(SuggestionContextMenu.actions()[0], ignoreAction)
             SuggestionMenu = QMenu('Spelling Suggestions')
             for word in self.checker.suggest(text):
                 word=word.decode('utf-8')
                 action = SuggestAction(word, SuggestionMenu)
                 action.correct.connect(self.replaceCorrect)
                 SuggestionMenu.addAction(action)
             if len(SuggestionMenu.actions()) != 0:
                SuggestionContextMenu.insertMenu(SuggestionContextMenu.actions()[0], SuggestionMenu)
     cursor.clearSelection()
     SuggestionContextMenu.exec_(self.ui.textEdit.mapToGlobal(position))
     self.ui.textEdit.setTextCursor(cursor)
Exemple #5
0
    def show_context_menu(self, point):
        self.context_item = self.table.itemAt(point)
        case_menu = QMenu(_('Change Case'))
        action_upper_case = case_menu.addAction(_('Upper Case'))
        action_lower_case = case_menu.addAction(_('Lower Case'))
        action_swap_case = case_menu.addAction(_('Swap Case'))
        action_title_case = case_menu.addAction(_('Title Case'))
        action_capitalize = case_menu.addAction(_('Capitalize'))

        action_upper_case.triggered.connect(self.upper_case)
        action_lower_case.triggered.connect(self.lower_case)
        action_swap_case.triggered.connect(self.swap_case)
        action_title_case.triggered.connect(self.title_case)
        action_capitalize.triggered.connect(self.capitalize)

        m = self.au_context_menu = QMenu()
        ca = m.addAction(_('Copy'))
        ca.triggered.connect(self.copy_to_clipboard)
        ca = m.addAction(_('Paste'))
        ca.triggered.connect(self.paste_from_clipboard)
        m.addSeparator()

        if self.context_item is not None and self.context_item.column() == 0:
            ca = m.addAction(_('Copy to author sort'))
            ca.triggered.connect(self.copy_au_to_aus)
        else:
            ca = m.addAction(_('Copy to author'))
            ca.triggered.connect(self.copy_aus_to_au)
        m.addSeparator()
        m.addMenu(case_menu)
        m.exec_(self.table.mapToGlobal(point))
Exemple #6
0
 def show_context_menu(self, point):
     item = self.folders.itemAt(point)
     if item is None:
         return
     m = QMenu(self)
     m.addAction(QIcon(I('mimetypes/dir.png')), _('Create new folder'), partial(self.create_folder, item))
     m.popup(self.folders.mapToGlobal(point))
Exemple #7
0
    def contextMenuEvent(self, event):  #pos):
        popup_menu = self.textEdit.createStandardContextMenu()  #pos)

        # Select the word under the cursor.
        cursor = self.textEdit.textCursor()
        cursor.select(QTextCursor.WordUnderCursor)
        self.textEdit.setTextCursor(cursor)

        # Check if the selected word is misspelled and offer spelling
        # suggestions if it is.
        if self.textEdit.textCursor().hasSelection():
            text = unicode(self.textEdit.textCursor().selectedText())
            if not self.dict.check(text):
                #print "o0mad to0 dict :D"
                spell_menu = QMenu(u'شاید منظورتان این بوده:')
                #print spell_menu
                for word in self.dict.suggest(text):
                    action = SpellAction(word, spell_menu)
                    action.correct.connect(self.correctWord)
                    spell_menu.addAction(action)
                    #print word
                # Only add the spelling suggests to the menu if there are
                # suggestions.
                print len(spell_menu.actions())
                #print spell_menu.actions()
                if len(spell_menu.actions()) != 0:
                    popup_menu.insertSeparator(popup_menu.actions()[0])
                    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)

        popup_menu.exec_(
            QCursor.
            pos())  #self.textEdit.mapToGlobal(QPoint(0, 0)))#.globalPos())
Exemple #8
0
 def create_toolbars(self):
     self.action_bar = b = self.addToolBar(_('File actions tool bar'))
     b.setObjectName('action_bar')  # Needed for saveState
     for x in ('undo', 'redo'):
         b.addAction(actions['editor-%s' % x])
     self.edit_bar = b = self.addToolBar(_('Edit actions tool bar'))
     for x in ('cut', 'copy', 'paste'):
         b.addAction(actions['editor-%s' % x])
     self.tools_bar = b = self.addToolBar(_('Editor tools'))
     if self.syntax == 'html':
         b.addAction(actions['fix-html-current'])
     if self.syntax in {'xml', 'html', 'css'}:
         b.addAction(actions['pretty-current'])
     if self.syntax in {'html', 'css'}:
         b.addAction(actions['insert-image'])
     if self.syntax == 'html':
         self.format_bar = b = self.addToolBar(_('Format text'))
         for x in ('bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'color', 'background-color'):
             b.addAction(actions['format-text-%s' % x])
         ac = b.addAction(QIcon(I('format-text-heading.png')), _('Change paragraph to heading'))
         m = QMenu()
         ac.setMenu(m)
         b.widgetForAction(ac).setPopupMode(QToolButton.InstantPopup)
         for name in tuple('h%d' % d for d in range(1, 7)) + ('p',):
             m.addAction(actions['rename-block-tag-%s' % name])
 def show_context_menu(self, point):
     idx = self.currentIndex()
     if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject():
         m = QMenu()
         m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover)
         m.addAction(QIcon(I('edit-copy.png')), _('Copy this cover to clipboard'), self.copy_cover)
         m.exec_(QCursor.pos())
Exemple #10
0
 def build_menu(self, action):
     m = action.menu()
     ac = MenuAction(action, self)
     if m is None:
         m = QMenu()
         m.addAction(action)
     ac.setMenu(m)
     return ac
Exemple #11
0
 def build_context_menu(self):
     cm = QMenu(self)
     paste = cm.addAction(_('Paste Cover'))
     copy = cm.addAction(_('Copy Cover'))
     if not QApplication.instance().clipboard().mimeData().hasImage():
         paste.setEnabled(False)
     copy.triggered.connect(self.copy_to_clipboard)
     paste.triggered.connect(self.paste_from_clipboard)
     return cm
Exemple #12
0
 def contextMenuEvent(self, ev):
     cm = QMenu(self)
     paste = cm.addAction(_("Paste Cover"))
     copy = cm.addAction(_("Copy Cover"))
     if not QApplication.instance().clipboard().mimeData().hasImage():
         paste.setEnabled(False)
     copy.triggered.connect(self.copy_to_clipboard)
     paste.triggered.connect(self.paste_from_clipboard)
     cm.exec_(ev.globalPos())
Exemple #13
0
 def create_application_menubar(cls):
     mb = QMenuBar(None)
     menu = QMenu()
     for action in cls.get_menubar_actions():
         menu.addAction(action)
         cls.__actions.append(action)
         yield action
     mb.addMenu(menu)
     cls.___menu_bar = mb
     cls.___menu = menu
Exemple #14
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)
     m.exec_(ev.globalPos())
Exemple #15
0
class MenuBar(QMenuBar):  # {{{

    def __init__(self, location_manager, parent):
        QMenuBar.__init__(self, parent)
        self.gui = parent
        self.setNativeMenuBar(True)

        self.location_manager = location_manager
        self.added_actions = []

        self.donate_action = QAction(_('Donate'), self)
        self.donate_menu = QMenu()
        self.donate_menu.addAction(self.gui.donate_action)
        self.donate_action.setMenu(self.donate_menu)

    def update_lm_actions(self):
        for ac in self.added_actions:
            clone = getattr(ac, 'clone', None)
            if clone is not None and clone in self.location_manager.all_actions:
                ac.setVisible(clone in self.location_manager.available_actions)

    def init_bar(self, actions):
        for ac in self.added_actions:
            m = ac.menu()
            if m is not None:
                m.setVisible(False)

        self.clear()
        self.added_actions = []

        for what in actions:
            if what is None:
                continue
            elif what == 'Location Manager':
                for ac in self.location_manager.all_actions:
                    ac = self.build_menu(ac)
                    self.addAction(ac)
                    self.added_actions.append(ac)
                    ac.setVisible(False)
            elif what == 'Donate':
                self.addAction(self.donate_action)
            elif what in self.gui.iactions:
                action = self.gui.iactions[what]
                ac = self.build_menu(action.qaction)
                self.addAction(ac)
                self.added_actions.append(ac)

    def build_menu(self, action):
        m = action.menu()
        ac = MenuAction(action, self)
        if m is None:
            m = QMenu()
            m.addAction(action)
        ac.setMenu(m)
        return ac
Exemple #16
0
 def show_context_menu(self, pos):
     m = QMenu(self)
     a = m.addAction
     for x in ('undo', 'redo'):
         a(actions['editor-%s' % x])
     m.addSeparator()
     for x in ('cut', 'copy', 'paste'):
         a(actions['editor-' + x])
     m.addSeparator()
     m.addAction(_('&Select all'), self.editor.select_all)
     m.addAction(actions['mark-selected-text'])
     m.exec_(self.editor.mapToGlobal(pos))
Exemple #17
0
    def create_action(self, spec=None, attr='qaction', shortcut_name=None):
        if spec is None:
            spec = self.action_spec
        text, icon, tooltip, shortcut = spec
        if icon is not None:
            action = QAction(QIcon(I(icon)), text, self.gui)
        else:
            action = QAction(text, self.gui)
        if attr == 'qaction':
            mt = (action.text() if self.action_menu_clone_qaction is True else
                    unicode(self.action_menu_clone_qaction))
            self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui)
            ma.triggered.connect(action.trigger)
        for a in ((action, ma) if attr == 'qaction' else (action,)):
            a.setAutoRepeat(self.auto_repeat)
            text = tooltip if tooltip else text
            a.setToolTip(text)
            a.setStatusTip(text)
            a.setWhatsThis(text)
        shortcut_action = action
        desc = tooltip if tooltip else None
        if attr == 'qaction':
            shortcut_action = ma
        if shortcut is not None:
            keys = ((shortcut,) if isinstance(shortcut, basestring) else
                    tuple(shortcut))
            if shortcut_name is None and spec[0]:
                shortcut_name = unicode(spec[0])

            if shortcut_name and self.action_spec[0] and not (
                    attr == 'qaction' and self.popup_type == QToolButton.InstantPopup):
                try:
                    self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr,
                        shortcut_name, default_keys=keys,
                        action=shortcut_action, description=desc,
                        group=self.action_spec[0])
                except NameConflict as e:
                    try:
                        prints(unicode(e))
                    except:
                        pass
                    shortcut_action.setShortcuts([QKeySequence(key,
                        QKeySequence.PortableText) for key in keys])


        if attr is not None:
            setattr(self, attr, action)
        if attr == 'qaction' and self.action_add_menu:
            menu = QMenu()
            action.setMenu(menu)
            if self.action_menu_clone_qaction:
                menu.addAction(self.menuless_qaction)
        return action
Exemple #18
0
 def show_context_menu(self, pos):
     menu = QMenu(self)
     menu.addAction(actions['edit-toc'])
     menu.addAction(_('&Expand all'), self.view.expandAll)
     menu.addAction(_('&Collapse all'), self.view.collapseAll)
     menu.addAction(self.refresh_action)
     menu.exec_(self.view.mapToGlobal(pos))
Exemple #19
0
 def contextMenuEvent(self, event):
     index = self.indexAt(event.pos())
     
     if not index.isValid():
         return
     
     result = self.model().get_result(index)
     
     menu = QMenu()
     da = menu.addAction(_('Download...'), partial(self.download_requested.emit, result))
     if not result.downloads:
         da.setEnabled(False)
     menu.addSeparator()
     menu.addAction(_('Goto in store...'), partial(self.open_requested.emit, result))
     menu.exec_(event.globalPos())
    def genesis(self):
        icon_resources = self.load_resources(PLUGIN_ICONS)
        set_plugin_icon_resources(self.name, icon_resources)

        self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
        self.old_actions_unique_map = {}
        self.us = UnitedStates()

        res = self.load_resources(PORTABLE_RESOURCES)

        os.makedirs(os.path.join(self.us.portable_directory,
                                 'portable'))
        os.makedirs(os.path.join(self.us.portable_directory,
                                 'portable/bootstrap'))
        os.makedirs(os.path.join(self.us.portable_directory,
                                 'portable/bootstrap/css'))
        os.makedirs(os.path.join(self.us.portable_directory,
                                 'portable/bootstrap/js'))
        os.makedirs(os.path.join(self.us.portable_directory,
                                 'portable/bootstrap/fonts'))

        for resource in res.keys():
            if resource == "portable/libraries.js":
                lib_lines = res[resource].split(os.linesep)
                lib_lines.insert(4, "var PORTABLE = true;{}".format(os.linesep))
                with open(os.path.join(self.us.portable_directory,
                                       'portable/libraries.js'), "w") as lib:
                    lib.writelines(os.linesep.join(lib_lines))
            else:
                with open(os.path.join(self.us.portable_directory,
                                       resource), 'wb') as portable:
                    portable.write(res[resource])

        self.popup_type = QToolButton.InstantPopup
        base_plugin_object = self.interface_action_base_plugin
        do_user_config = base_plugin_object.do_user_config

        self.d = LetsShareBooksDialog(self.gui,
                                      self.qaction.icon(),
                                      do_user_config,
                                      self.qaction, self.us)
        m = QMenu(self.gui)
        self.qaction.setMenu(m)
        a = QWidgetAction(m)
        a.setDefaultWidget(self.d)
        m.addAction(a)
Exemple #21
0
    def genesis(self):
        icon_resources = self.load_resources(PLUGIN_ICONS)
        set_plugin_icon_resources(self.name, icon_resources)

        self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
        self.old_actions_unique_map = {}
        self.us = UnitedStates()

        self.popup_type = QToolButton.InstantPopup
        base_plugin_object = self.interface_action_base_plugin
        do_user_config = base_plugin_object.do_user_config

        d = LetsShareBooksDialog(self.gui, self.qaction.icon(), do_user_config, self.qaction, self.us)
        m = QMenu(self.gui)
        self.qaction.setMenu(m)
        a = QWidgetAction(m)
        a.setDefaultWidget(d)
        m.addAction(a)
Exemple #22
0
    def eventFilter(self, obj, event):
        base = super(Central, self)
        if obj is not self.editor_tabs.tabBar() or event.type() != QEvent.MouseButtonPress or event.button() not in (Qt.RightButton, Qt.MidButton):
            return base.eventFilter(obj, event)
        index = self.editor_tabs.tabBar().tabAt(event.pos())
        if index < 0:
            return base.eventFilter(obj, event)
        if event.button() == Qt.MidButton:
            self._close_requested(index)
        ed = self.editor_tabs.widget(index)
        if ed is not None:
            menu = QMenu(self)
            menu.addAction(actions['close-current-tab'].icon(), _('Close tab'), partial(self.close_requested.emit, ed))
            menu.addSeparator()
            menu.addAction(actions['close-all-but-current-tab'].icon(), _('Close other tabs'), partial(self.close_all_but, ed))
            menu.exec_(self.editor_tabs.tabBar().mapToGlobal(event.pos()))

        return True
Exemple #23
0
    def contextMenuEvent(self, event):
        menu = self.createStandardContextMenu()
        menu.addSeparator()

        case_menu = QMenu(_("Change Case"))
        action_upper_case = case_menu.addAction(_("Upper Case"))
        action_lower_case = case_menu.addAction(_("Lower Case"))
        action_swap_case = case_menu.addAction(_("Swap Case"))
        action_title_case = case_menu.addAction(_("Title Case"))
        action_capitalize = case_menu.addAction(_("Capitalize"))

        self.connect(action_upper_case, SIGNAL("triggered()"), self.upper_case)
        self.connect(action_lower_case, SIGNAL("triggered()"), self.lower_case)
        self.connect(action_swap_case, SIGNAL("triggered()"), self.swap_case)
        self.connect(action_title_case, SIGNAL("triggered()"), self.title_case)
        self.connect(action_capitalize, SIGNAL("triggered()"), self.capitalize)

        menu.addMenu(case_menu)
        menu.exec_(event.globalPos())
Exemple #24
0
    def contextMenuEvent(self, event):
        menu = self.createStandardContextMenu()
        menu.addSeparator()

        case_menu = QMenu(_('Change Case'))
        action_upper_case = case_menu.addAction(_('Upper Case'))
        action_lower_case = case_menu.addAction(_('Lower Case'))
        action_swap_case = case_menu.addAction(_('Swap Case'))
        action_title_case = case_menu.addAction(_('Title Case'))
        action_capitalize = case_menu.addAction(_('Capitalize'))

        action_upper_case.triggered[()].connect(self.upper_case)
        action_lower_case.triggered[()].connect(self.lower_case)
        action_swap_case.triggered[()].connect(self.swap_case)
        action_title_case.triggered[()].connect(self.title_case)
        action_capitalize.triggered[()].connect(self.capitalize)

        menu.addMenu(case_menu)
        menu.exec_(event.globalPos())
 def contextMenuEvent(self, event):
     menu = QMenu(self)
     pos = event.pos()
     index = self.indexAt(pos)
     if index.isValid():
         item = self.indexAt(pos).internalPointer()
         node = self.item_to_node_dict[item]
         nodeset = [ node ] # ? ? ? ?
         optflag = False  # ? ? ? ?
         cmenu_spec = self.treemodel.make_cmenuspec_for_set(nodeset, optflag)
         for x in cmenu_spec:
             if x is not None:
                 str, thunk = x[:2]
                 act = QAction(str, self)
                 act.setEnabled("disabled" not in x[2:])
                 self.connect(act, SIGNAL("triggered()"), thunk)
                 menu.addAction(act)
             else:
                 menu.addSeparator()
         menu.exec_(event.globalPos())
 def displayContextMenu(self, pos):
     """
     type pos: int
     """
     menu = QMenu()
     removeAction = menu.addAction("Remove")
     action = menu.exec_(self.mapToGlobal(pos))
     if action == removeAction:
         while self.filterTable.rowCount() > self.row + 1:
             self.filterTable.removeRow(self.filterTable.rowCount() - 1)
         self.filterTable.removeCellWidget(self.row, self.filtTableConnectCol)
Exemple #27
0
    def contextMenuEvent(self, ev):
        m = QMenu(self)
        w = self.model().word_for_row(self.currentIndex().row())
        if w is not None:
            a = m.addAction(_('Change %s to') % w[0])
            cm = QMenu()
            a.setMenu(cm)
            cm.addAction(_('Specify replacement manually'), partial(self.change_to.emit, w, None))
            cm.addSeparator()
            for s in dictionaries.suggestions(*w):
                cm.addAction(s, partial(self.change_to.emit, w, s))

        m.addAction(_('Ignore/Unignore all selected words'), self.ignore_all)
        a = m.addAction(_('Add/Remove all selected words'))
        am = QMenu()
        a.setMenu(am)
        for dic in sorted(dictionaries.active_user_dictionaries, key=lambda x:sort_key(x.name)):
            am.addAction(dic.name, partial(self.add_all.emit, dic.name))

        m.exec_(ev.globalPos())
   def showSegContextMenu(self):
      menu = QMenu(self.segDefTableView)
      if len(self.segDefTableView.selectedIndexes())==0:
         return

      row = self.segDefTableView.selectedIndexes()[0].row()
      deleteSegMenuItem = menu.addAction("Delete Segment")
      action = menu.exec_(QCursor.pos())
      
      if action == deleteSegMenuItem:
         self.deleteSegRow(row)
   def showOrdContextMenu(self):
      menu = QMenu(self.segOrdListBox)
      if len(self.segOrdListBox.selectedItems())==0:
         return

      item = self.segOrdListBox.currentItem()
      deleteOrdMenuItem = menu.addAction("Delete Ordering")
      action = menu.exec_(QCursor.pos())
      
      if action == deleteOrdMenuItem:
         self.deleteOrdItem(item)
Exemple #30
0
 def setupWidgets(self):
     self.setWindowTitle(self.title)
     menubar = QMenuBar(self)
     menu = QMenu("Title " + self.title)
     self._action_tmp = QAction("Reproduce", self)
     signal_connect(self._action_tmp, SIGNAL("activated()"), self.reproduce)
     menu.addAction(self._action_tmp)
     menubar.addMenu(menu)
     menubar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
     self.setMenuBar(menubar)
     cw = QWidget(self)
     layout = QVBoxLayout(cw)
     cw.setLayout(layout)
     button = QPushButton("Reproduce", self)
     button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
     layout.addWidget(button)
     self.button = button
     signal_connect(button, SIGNAL("clicked()"), self.reproduce)        
     for o in ("menubar", "menu", "cw"): setattr(self, o, eval(o))
     
     self.setCentralWidget(cw)
Exemple #31
0
 def contextMenuEvent(self, ev):
     menu = QMenu(self)
     menu.addAction(actions['reload-preview'])
     menu.addAction(QIcon(I('debug.png')), _('Inspect element'), self.inspect)
     menu.exec_(ev.globalPos())
class Ui_DnaSequenceEditor(PM_DockWidget):
    """
    The Ui_DnaSequenceEditor class defines UI elements for the Sequence Editor
    object. The sequence editor is usually visible while in while editing
    a DnaStrand.

    It is a DockWidget that is docked at the bottom of the MainWindow.
    """
    _title = "Sequence Editor"
    _groupBoxCount = 0
    _lastGroupBox = None

    def __init__(self, win):
        """
        Constructor for the Ui_DnaSequenceEditor
        @param win: The parentWidget (MainWindow) for the sequence editor
        """

        self.win = win
        # Should parentWidget for a docwidget always be win?
        #Not necessary but most likely it will be the case.
        parentWidget = win

        _superclass.__init__(self, parentWidget, title=self._title)

        #A flag used to restore the state of the Reports dock widget
        #(which can be accessed through View  >  Reports) see self.show() and
        #self.closeEvent() for more details.
        self._reportsDockWidget_closed_in_show_method = False
        self.setFixedHeight(90)
        return

    def show(self):
        """
        Shows the sequence editor. While doing this, it also closes the reports
        dock widget (if visible) the state of the reports dockwidget will be
        restored when the sequence editor is closed.
        @see:self.closeEvent()
        """
        self._reportsDockWidget_closed_in_show_method = False
        #hide the history widget first
        #(It will be shown back during self.close)
        #The history widget is hidden or shown only when both
        # 'View > Full Screen' and View > Semi Full Screen actions
        # are *unchecked*
        #Thus show or close methods won't do anything to history widget
        # if either of the above mentioned actions is checked.
        if self.win.viewFullScreenAction.isChecked() or \
           self.win.viewSemiFullScreenAction.isChecked():
            pass
        else:
            if self.win.reportsDockWidget.isVisible():
                self.win.reportsDockWidget.close()
                self._reportsDockWidget_closed_in_show_method = True

        _superclass.show(self)
        return

    def closeEvent(self, event):
        """
        Overrides close event. Makes sure that the visible state of the reports
        widgetis restored when the sequence editor is closed.
        @see: self.show()
        """
        _superclass.closeEvent(self, event)

        if self.win.viewFullScreenAction.isChecked() or \
           self.win.viewSemiFullScreenAction.isChecked():
            pass
        else:
            if self._reportsDockWidget_closed_in_show_method:
                self.win.viewReportsAction.setChecked(True)
                self._reportsDockWidget_closed_in_show_method = False
        return

    def _loadWidgets(self):
        """
        Overrides PM.PM_DockWidget._loadWidgets. Loads the widget in this
        dockwidget.
        """
        self._loadMenuWidgets()
        self._loadTextEditWidget()
        return

    def _loadMenuWidgets(self):
        """
        Load the various menu widgets (e.g. Open, save sequence options,
        Find and replace widgets etc.
        """
        #Note: Find and replace widgets might be moved to their own class.

        self.loadSequenceButton = PM_ToolButton(
            self, iconPath="ui/actions/Properties Manager/Open.png")

        self.saveSequenceButton = PM_ToolButton(
            self,
            iconPath="ui/actions/Properties Manager/Save_Strand_Sequence.png")

        self.loadSequenceButton.setAutoRaise(True)
        self.saveSequenceButton.setAutoRaise(True)

        # Only supporting 5' to 3' direction until bug 2956 is fixed.
        # Mark 2008-12-19
        editDirectionChoices = ["5' to 3'"]  # , "3' to 5'"]
        self.baseDirectionChoiceComboBox = \
            PM_ComboBox( self,
                         choices = editDirectionChoices,
                         index     = 0,
                         spanWidth = False )

        #Find and replace widgets --
        self.findLineEdit = \
            PM_LineEdit( self,
                         label        = "",
                         spanWidth    = False)
        self.findLineEdit.setMaximumWidth(60)


        self.replaceLineEdit = \
            PM_LineEdit( self,
                         label        = "",
                         spanWidth    = False)
        self.replaceLineEdit.setMaximumWidth(60)

        self.findOptionsToolButton = PM_ToolButton(self)
        self.findOptionsToolButton.setMaximumWidth(12)
        self.findOptionsToolButton.setAutoRaise(True)

        self.findOptionsToolButton.setPopupMode(QToolButton.MenuButtonPopup)

        self._setFindOptionsToolButtonMenu()

        self.findNextToolButton = PM_ToolButton(
            self, iconPath="ui/actions/Properties Manager/Find_Next.png")
        self.findNextToolButton.setAutoRaise(True)

        self.findPreviousToolButton = PM_ToolButton(
            self, iconPath="ui/actions/Properties Manager/Find_Previous.png")
        self.findPreviousToolButton.setAutoRaise(True)

        self.replacePushButton = PM_PushButton(self, text="Replace")

        self.warningSign = QLabel(self)
        self.warningSign.setPixmap(
            getpixmap('ui/actions/Properties Manager/Warning.png'))
        self.warningSign.hide()

        self.phraseNotFoundLabel = QLabel(self)
        self.phraseNotFoundLabel.setText("Sequence Not Found")
        self.phraseNotFoundLabel.hide()

        # NOTE: Following needs cleanup in the PM_WidgetRow/ PM_WidgetGrid
        # but this explanation is sufficient  until thats done --

        # When the widget type starts with the word 'PM_' , the
        # PM_WidgetRow treats it as a well defined widget and thus doesn't try
        # to create a QWidget object (or its subclasses)
        # This is the reason why qLabels such as self.warningSign and
        # self.phraseNotFoundLabel  are defined as PM_Labels and not 'QLabels'
        # If they were defined as 'QLabel'(s) then PM_WidgetRow would have
        # recreated the label. Since we want to show/hide the above mentioned
        # labels (and if they were recreated as mentioned above),
        # we would have needed to define  those something like this:
        # self.phraseNotFoundLabel = widgetRow._widgetList[-2]
        #Cleanup in PM_widgetGrid could be to check if the widget starts with
        #'Q'  instead of 'PM_'

        #Widgets to include in the widget row.
        widgetList = [('PM_ToolButton', self.loadSequenceButton, 0),
                      ('PM_ToolButton', self.saveSequenceButton, 1),
                      ('QLabel', "     Sequence direction:", 2),
                      ('PM_ComboBox', self.baseDirectionChoiceComboBox, 3),
                      ('QLabel', "     Find:", 4),
                      ('PM_LineEdit', self.findLineEdit, 5),
                      ('PM_ToolButton', self.findOptionsToolButton, 6),
                      ('PM_ToolButton', self.findPreviousToolButton, 7),
                      ('PM_ToolButton', self.findNextToolButton, 8),
                      ('QLabel', "     Replace:", 9),
                      ('PM_TextEdit', self.replaceLineEdit, 10),
                      ('PM_PushButton', self.replacePushButton, 11),
                      ('PM_Label', self.warningSign, 12),
                      ('PM_Label', self.phraseNotFoundLabel, 13),
                      ('QSpacerItem', 5, 5, 14)]

        widgetRow = PM_WidgetRow(self,
                                 title='',
                                 widgetList=widgetList,
                                 label="",
                                 spanWidth=True)
        return

    def _loadTextEditWidget(self):
        """
        Load the SequenceTexteditWidgets.
        """
        self.sequenceTextEdit = \
            PM_TextEdit( self,
                         label = " Sequence: ",
                         spanWidth = False,
                         permit_enter_keystroke = False)
        self.sequenceTextEdit.setCursorWidth(2)
        self.sequenceTextEdit.setWordWrapMode(QTextOption.WrapAnywhere)
        self.sequenceTextEdit.setFixedHeight(20)

        #The StrandSequence 'Mate' it is a read only etxtedit that shows
        #the complementary strand sequence.
        self.sequenceTextEdit_mate = \
            PM_TextEdit(self,
                        label = "",
                        spanWidth = False,
                        permit_enter_keystroke = False
                        )
        palette = getPalette(None, QPalette.Base,
                             sequenceEditStrandMateBaseColor)
        self.sequenceTextEdit_mate.setPalette(palette)
        self.sequenceTextEdit_mate.setFixedHeight(20)
        self.sequenceTextEdit_mate.setReadOnly(True)
        self.sequenceTextEdit_mate.setWordWrapMode(QTextOption.WrapAnywhere)

        #Important to make sure that the horizontal and vertical scrollbars
        #for these text edits are never displayed.
        for textEdit in (self.sequenceTextEdit, self.sequenceTextEdit_mate):
            textEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            textEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        return

    def _getFindLineEditStyleSheet(self):
        """
        Return the style sheet for the findLineEdit. This sets the following
        properties only:
         - background-color

        This style is set whenever the searchStrig can't be found (sets
        a light red color background to the lineedit when this happens)

        @return: The line edit style sheet.
        @rtype:  str

        """
        styleSheet = "QLineEdit {"\
                   "background-color: rgb(255, 102, 102)"\
                   "}"
        #Not used:
        #  background-color: rgb(217, 255, 216)\

        return styleSheet

    def _setFindOptionsToolButtonMenu(self):
        """
        Sets the menu for the findOptionstoolbutton that appears a small
        menu button next to the findLineEdit.
        """
        self.findOptionsMenu = QMenu(self.findOptionsToolButton)

        self.caseSensitiveFindAction = QAction(self.findOptionsToolButton)
        self.caseSensitiveFindAction.setText('Match Case')
        self.caseSensitiveFindAction.setCheckable(True)
        self.caseSensitiveFindAction.setChecked(False)

        self.findOptionsMenu.addAction(self.caseSensitiveFindAction)
        self.findOptionsMenu.addSeparator()

        self.findOptionsToolButton.setMenu(self.findOptionsMenu)
        return

    def _addToolTipText(self):
        """
            What's Tool Tip text for widgets in this Property Manager.
            """
        from ne1_ui.ToolTipText_for_PropertyManagers import ToolTip_DnaSequenceEditor
        ToolTip_DnaSequenceEditor(self)
        return

    def _addWhatsThisText(self):
        """
            What's This text for widgets in this Property Manager.

            """
        from ne1_ui.WhatsThisText_for_PropertyManagers import whatsThis_DnaSequenceEditor
        whatsThis_DnaSequenceEditor(self)
        return
Exemple #33
0
    def show_context_menu(self, point):
        item = self.currentItem()

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

        if item is not None:
            m = QMenu()
            ci = unicode(item.data(0, Qt.DisplayRole).toString())
            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)
            m.addAction(QIcon(I('trash.png')), _('Remove all selected items'),
                        self.del_items)
            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.addAction(QIcon(I('edit_input.png')),
                        _('Change the location this entry points to'),
                        self.edit_item)
            m.addAction(_('Change all selected items to title case'),
                        self.title_case)
            m.addAction(QIcon(I('modified.png')),
                        _('Bulk rename all selected items'), self.bulk_rename)
            m.exec_(QCursor.pos())
class EditorWidget(QWebView):  # {{{
    def __init__(self, parent=None):
        QWebView.__init__(self, parent)

        self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)

        extra_shortcuts = {
            'ToggleBold': 'Bold',
            'ToggleItalic': 'Italic',
            'ToggleUnderline': 'Underline',
        }

        for wac, name, icon, text, checkable in [
            ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True),
            ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'),
             True),
            ('ToggleUnderline', 'underline', 'format-text-underline',
             _('Underline'), True),
            ('ToggleStrikethrough', 'strikethrough',
             'format-text-strikethrough', _('Strikethrough'), True),
            ('ToggleSuperscript', 'superscript', 'format-text-superscript',
             _('Superscript'), True),
            ('ToggleSubscript', 'subscript', 'format-text-subscript',
             _('Subscript'), True),
            ('InsertOrderedList', 'ordered_list', 'format-list-ordered',
             _('Ordered list'), True),
            ('InsertUnorderedList', 'unordered_list', 'format-list-unordered',
             _('Unordered list'), True),
            ('AlignLeft', 'align_left', 'format-justify-left', _('Align left'),
             False),
            ('AlignCenter', 'align_center', 'format-justify-center',
             _('Align center'), False),
            ('AlignRight', 'align_right', 'format-justify-right',
             _('Align right'), False),
            ('AlignJustified', 'align_justified', 'format-justify-fill',
             _('Align justified'), False),
            ('Undo', 'undo', 'edit-undo', _('Undo'), False),
            ('Redo', 'redo', 'edit-redo', _('Redo'), False),
            ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'),
             False),
            ('Copy', 'copy', 'edit-copy', _('Copy'), False),
            ('Paste', 'paste', 'edit-paste', _('Paste'), False),
            ('Cut', 'cut', 'edit-cut', _('Cut'), False),
            ('Indent', 'indent', 'format-indent-more',
             _('Increase Indentation'), False),
            ('Outdent', 'outdent', 'format-indent-less',
             _('Decrease Indentation'), False),
            ('SelectAll', 'select_all', 'edit-select-all', _('Select all'),
             False),
        ]:
            ac = PageAction(wac, icon, text, checkable, self)
            setattr(self, 'action_' + name, ac)
            ss = extra_shortcuts.get(wac, None)
            if ss:
                ac.setShortcut(QKeySequence(getattr(QKeySequence, ss)))
            if wac == 'RemoveFormat':
                ac.triggered.connect(self.remove_format_cleanup,
                                     type=Qt.QueuedConnection)

        self.action_color = QAction(QIcon(I('format-text-color')),
                                    _('Foreground color'), self)
        self.action_color.triggered.connect(self.foreground_color)

        self.action_background = QAction(QIcon(I('format-fill-color')),
                                         _('Background color'), self)
        self.action_background.triggered.connect(self.background_color)

        self.action_block_style = QAction(QIcon(I('format-text-heading')),
                                          _('Style text block'), self)
        self.action_block_style.setToolTip(_('Style the selected text block'))
        self.block_style_menu = QMenu(self)
        self.action_block_style.setMenu(self.block_style_menu)
        self.block_style_actions = []
        for text, name in [
            (_('Normal'), 'p'),
            (_('Heading') + ' 1', 'h1'),
            (_('Heading') + ' 2', 'h2'),
            (_('Heading') + ' 3', 'h3'),
            (_('Heading') + ' 4', 'h4'),
            (_('Heading') + ' 5', 'h5'),
            (_('Heading') + ' 6', 'h6'),
            (_('Pre-formatted'), 'pre'),
            (_('Blockquote'), 'blockquote'),
            (_('Address'), 'address'),
        ]:
            ac = BlockStyleAction(text, name, self)
            self.block_style_menu.addAction(ac)
            self.block_style_actions.append(ac)

        self.action_insert_link = QAction(QIcon(I('insert-link.png')),
                                          _('Insert link'), self)
        self.action_insert_link.triggered.connect(self.insert_link)
        self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self)
        self.action_clear.triggered.connect(self.clear_text)

        self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
        self.page().linkClicked.connect(self.link_clicked)

        self.setHtml('')
        self.page().setContentEditable(True)

    def clear_text(self, *args):
        us = self.page().undoStack()
        us.beginMacro('clear all text')
        self.action_select_all.trigger()
        self.action_remove_format.trigger()
        self.exec_command('delete')
        us.endMacro()
        self.set_font_style()
        self.setFocus(Qt.OtherFocusReason)

    def link_clicked(self, url):
        open_url(url)

    def foreground_color(self):
        col = QColorDialog.getColor(Qt.black, self,
                                    _('Choose foreground color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('foreColor', unicode(col.name()))

    def background_color(self):
        col = QColorDialog.getColor(Qt.white, self,
                                    _('Choose background color'),
                                    QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('hiliteColor', unicode(col.name()))

    def insert_link(self, *args):
        link, ok = QInputDialog.getText(self, _('Create link'), _('Enter URL'))
        if not ok:
            return
        url = self.parse_link(unicode(link))
        if url.isValid():
            url = unicode(url.toString())
            self.exec_command('createLink', url)

    def parse_link(self, link):
        link = link.strip()
        has_schema = re.match(r'^[a-zA-Z]+:', link)
        if has_schema is not None:
            url = QUrl(link, QUrl.TolerantMode)
            if url.isValid():
                return url
        if os.path.exists(link):
            return QUrl.fromLocalFile(link)

        if has_schema is None:
            first, _, rest = link.partition('.')
            prefix = 'http'
            if first == 'ftp':
                prefix = 'ftp'
            url = QUrl(prefix + '://' + link, QUrl.TolerantMode)
            if url.isValid():
                return url

        return QUrl(link, QUrl.TolerantMode)

    def sizeHint(self):
        return QSize(150, 150)

    def exec_command(self, cmd, arg=None):
        frame = self.page().mainFrame()
        if arg is not None:
            js = 'document.execCommand("%s", false, "%s");' % (cmd, arg)
        else:
            js = 'document.execCommand("%s", false, null);' % cmd
        frame.evaluateJavaScript(js)

    def remove_format_cleanup(self):
        self.html = self.html

    @dynamic_property
    def html(self):
        def fget(self):
            ans = u''
            check = unicode(self.page().mainFrame().toPlainText()).strip()
            if not check:
                return ans
            try:
                raw = unicode(self.page().mainFrame().toHtml())
                raw = xml_to_unicode(raw,
                                     strip_encoding_pats=True,
                                     resolve_entities=True)[0]
                raw = self.comments_pat.sub('', raw)

                try:
                    root = html.fromstring(raw)
                except:
                    root = fromstring(raw)

                elems = []
                for body in root.xpath('//body'):
                    if body.text:
                        elems.append(body.text)
                    elems += [
                        html.tostring(x, encoding=unicode) for x in body
                        if x.tag not in ('script', 'style')
                    ]

                if len(elems) > 1:
                    ans = u'<div>%s</div>' % (u''.join(elems))
                else:
                    ans = u''.join(elems)
                    if not ans.startswith('<'):
                        ans = '<p>%s</p>' % ans
                ans = xml_replace_entities(ans)
            except:
                import traceback
                traceback.print_exc()

            return ans

        def fset(self, val):
            self.setHtml(val)
            self.set_font_style()

        return property(fget=fget, fset=fset)

    def set_font_style(self):
        fi = QFontInfo(QApplication.font(self))
        f = fi.pixelSize() + 1 + int(
            tweaks['change_book_details_font_size_by'])
        fam = unicode(fi.family()).strip().replace('"', '')
        if not fam:
            fam = 'sans-serif'
        style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam)

        # toList() is needed because PyQt on Debian is old/broken
        for body in self.page().mainFrame().documentElement().findAll(
                'body').toList():
            body.setAttribute('style', style)
        self.page().setContentEditable(True)

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
            ev.ignore()
        else:
            return QWebView.keyPressEvent(self, ev)

    def keyReleaseEvent(self, ev):
        if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
            ev.ignore()
        else:
            return QWebView.keyReleaseEvent(self, ev)
Exemple #35
0
    def contextMenuEvent(self, event):
        """ Creates two context menus:
            1. no modifier -> spellchecker & clear emnu
            2. ctrl modifier -> Text change & Insert symbol
        """
        contextMenu = self.createStandardContextMenu()
        spellMenu = True

        if (QApplication.keyboardModifiers() & Qt.ControlModifier):
            spellMenu = False

        self.clearAction = QtGui.QAction(self.tr("Clear"), contextMenu)
        contextMenu.addSeparator()
        contextMenu.addAction(self.clearAction)
        if not len(self.toPlainText()):
            self.clearAction.setEnabled(False)
        QtCore.QObject.connect(self.clearAction, QtCore.SIGNAL("triggered()"),
                               self.clear)
        if not spellMenu:
            textOpsMenu = QMenu(self.tr("Text change..."))

            removeEOLAction = QtGui.QAction(
                self.tr("Join lines"),
                textOpsMenu,
            )
            textOpsMenu.addAction(removeEOLAction)
            QtCore.QObject.connect(removeEOLAction,
                                   QtCore.SIGNAL("triggered()"),
                                   self.removeEOL)

            textOpsMenu.addSeparator()

            toUppercaseAction = QtGui.QAction(self.tr("to UPPERCASE"),
                                              textOpsMenu)
            textOpsMenu.addAction(toUppercaseAction)
            QtCore.QObject.connect(toUppercaseAction,
                                   QtCore.SIGNAL("triggered()"),
                                   self.toUppercase)

            toLowercaseAction = QtGui.QAction(self.tr("to lowercase"),
                                              textOpsMenu)
            textOpsMenu.addAction(toLowercaseAction)
            QtCore.QObject.connect(toLowercaseAction,
                                   QtCore.SIGNAL("triggered()"),
                                   self.toLowercase)

            toTitleAction = QtGui.QAction(self.tr("to Title"), textOpsMenu)
            textOpsMenu.addAction(toTitleAction)
            QtCore.QObject.connect(toTitleAction, QtCore.SIGNAL("triggered()"),
                                   self.toTitlecase)

            toCapsAction = QtGui.QAction(self.tr("to Capitalize"), textOpsMenu)
            textOpsMenu.addAction(toCapsAction)
            QtCore.QObject.connect(toCapsAction, QtCore.SIGNAL("triggered()"),
                                   self.toCaps)

            contextMenu.insertSeparator(contextMenu.actions()[0])
            contextMenu.insertMenu(contextMenu.actions()[0], textOpsMenu)

            insertSymbolMenu = QMenu(self.tr("Insert symbol..."))
            settings_symbols = settings.get('editor:symbols')
            if settings_symbols:
                self.symbols = settings_symbols.split('\n')
            for symbol in self.symbols:
                action = SpellAction(symbol, insertSymbolMenu)
                action.correct.connect(self.insertSymbol)
                insertSymbolMenu.addAction(action)

            contextMenu.insertMenu(contextMenu.actions()[0], insertSymbolMenu)

        if not self.textCursor().hasSelection() and spellMenu:
            # Select the word under the cursor for spellchecker
            cursor = self.textCursor()
            cursor.select(QTextCursor.WordUnderCursor)

            self.setTextCursor(cursor)
            text = unicode(self.textCursor().selectedText())

            #TODO: put to configuration list of ignored starting/ending chars
            # remove u"„" from selection
            if text.startswith(u"„") or text.startswith(u"“"):
                text = text[1:]
                selectionEnd = cursor.selectionEnd()
                cursor.setPosition(cursor.position() - len(text))
                cursor.setPosition(selectionEnd, QTextCursor.KeepAnchor)
                self.setTextCursor(cursor)
            # remove u"”" from selection
            if text.endswith(u"”") or text.startswith(u"“"):
                selectionEnd = cursor.selectionEnd()
                cursor.setPosition(cursor.position() - len(text))
                cursor.setPosition(selectionEnd - 1, QTextCursor.KeepAnchor)
                text = text[:-1]
                self.setTextCursor(cursor)

            # Check if the selected word is misspelled and offer spelling
            # suggestions if it is.
            if self.textCursor().hasSelection():
                if not self.dict.check(text):
                    spell_menu = QMenu(self.tr("Spelling Suggestions"))
                    addWordAcction = QAction(self.tr('Add word...'),
                                             spell_menu)
                    QtCore.QObject.connect(addWordAcction,
                                           QtCore.SIGNAL("triggered()"),
                                           self.addWord)
                    #addWordAcction.triggered.connect(self.addWord)
                    spell_menu.addAction(addWordAcction)
                    for word in self.dict.suggest(text):
                        action = SpellAction(word, spell_menu)
                        action.correct.connect(self.changeText)
                        spell_menu.addAction(action)
                    contextMenu.insertSeparator(contextMenu.actions()[1])
                    contextMenu.insertMenu(contextMenu.actions()[0],
                                           spell_menu)
                    # Only add the spelling suggests to the menu if there are
                    # suggestions.
                    if len(spell_menu.actions()) != 1:
                        spell_menu.insertSeparator(spell_menu.actions()[1])

        contextMenu.exec_(event.globalPos())
        event.accept()
Exemple #36
0
    def show_context_menu(self, pos):
        m = QMenu(self)
        a = m.addAction
        c = self.editor.cursorForPosition(pos)
        fmt = self.editor.syntax_format_for_cursor(c)
        spell = fmt.property(
            SPELL_PROPERTY).toPyObject() if fmt is not None else None
        if spell is not None:
            word, locale = spell
            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)
            if found:
                suggestions = dictionaries.suggestions(word, locale)[:7]
                if suggestions:
                    for suggestion in suggestions:
                        ac = m.addAction(
                            suggestion,
                            partial(self.editor.simple_replace, suggestion))
                        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()

        for x in ('undo', 'redo'):
            a(actions['editor-%s' % x])
        m.addSeparator()
        for x in ('cut', 'copy', 'paste'):
            a(actions['editor-' + x])
        m.addSeparator()
        m.addAction(_('&Select all'), self.editor.select_all)
        m.addAction(actions['mark-selected-text'])
        if self.syntax == 'html':
            m.addAction(actions['multisplit'])
        m.exec_(self.editor.mapToGlobal(pos))
Exemple #37
0
    def __init__(self, parent, rc, imgman):
        """An ImageControlDialog is initialized with a parent widget, a RenderControl object,
        and an ImageManager object"""
        QDialog.__init__(self, parent)
        image = rc.image
        self.setWindowTitle("%s: Colour Controls" % image.name)
        self.setWindowIcon(pixmaps.colours.icon())
        self.setModal(False)
        self.image = image
        self._rc = rc
        self._imgman = imgman
        self._currier = PersistentCurrier()

        # init internal state
        self._prev_range = self._display_range = None, None
        self._hist = None
        self._geometry = None

        # create layouts
        lo0 = QVBoxLayout(self)
        #    lo0.setContentsMargins(0,0,0,0)

        # histogram plot
        whide = self.makeButton("Hide", self.hide, width=128)
        whide.setShortcut(Qt.Key_F9)
        lo0.addWidget(Separator(self, "Histogram and ITF", extra_widgets=[whide]))
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        self._histplot = QwtPlot(self)
        self._histplot.setAutoDelete(False)
        lo1.addWidget(self._histplot, 1)
        lo2 = QHBoxLayout()
        lo2.setContentsMargins(0, 0, 0, 0)
        lo2.setSpacing(2)
        lo0.addLayout(lo2)
        lo0.addLayout(lo1)
        self._wautozoom = QCheckBox("autozoom", self)
        self._wautozoom.setChecked(True)
        self._wautozoom.setToolTip("""<P>If checked, then the histrogram plot will zoom in automatically when
      you narrow the current intensity range.</P>""")
        self._wlogy = QCheckBox("log Y", self)
        self._wlogy.setChecked(True)
        self._ylogscale = True
        self._wlogy.setToolTip(
            """<P>If checked, a log-scale Y axis is used for the histogram plot instead of a linear one.""")
        QObject.connect(self._wlogy, SIGNAL("toggled(bool)"), self._setHistLogScale)
        self._whistunzoom = self.makeButton("", self._unzoomHistogram, icon=pixmaps.full_range.icon())
        self._whistzoomout = self.makeButton("-", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(.1)))
        self._whistzoomin = self.makeButton("+", self._currier.curry(self._zoomHistogramByFactor, math.sqrt(10)))
        self._whistzoomin.setToolTip("""<P>Click to zoom into the histogram plot by one step. This does not
      change the current intensity range.</P>""")
        self._whistzoomout.setToolTip("""<P>Click to zoom out of the histogram plot by one step. This does not
      change the current intensity range.</P>""")
        self._whistunzoom.setToolTip("""<P>Click to reset the histogram plot back to its full extent.
      This does not change the current intensity range.</P>""")
        self._whistzoom = QwtWheel(self)
        self._whistzoom.setOrientation(Qt.Horizontal)
        self._whistzoom.setMaximumWidth(80)
        self._whistzoom.setRange(10, 0)
        self._whistzoom.setStep(0.1)
        self._whistzoom.setTickCnt(30)
        self._whistzoom.setTracking(False)
        QObject.connect(self._whistzoom, SIGNAL("valueChanged(double)"), self._zoomHistogramFinalize)
        QObject.connect(self._whistzoom, SIGNAL("sliderMoved(double)"), self._zoomHistogramPreview)
        self._whistzoom.setToolTip("""<P>Use this wheel control to zoom in/out of the histogram plot.
      This does not change the current intensity range.
      Note that the zoom wheel should also respond to your mouse wheel, if you have one.</P>""")
        # This works around a stupid bug in QwtSliders -- when using the mousewheel, only sliderMoved() signals are emitted,
        # with no final  valueChanged(). If we want to do a fast preview of something on sliderMoved(), and a "slow" final
        # step on valueChanged(), we're in trouble. So we start a timer on sliderMoved(), and if the timer expires without
        # anything else happening, do a valueChanged().
        # Here we use a timer to call zoomHistogramFinalize() w/o an argument.
        self._whistzoom_timer = QTimer(self)
        self._whistzoom_timer.setSingleShot(True)
        self._whistzoom_timer.setInterval(500)
        QObject.connect(self._whistzoom_timer, SIGNAL("timeout()"), self._zoomHistogramFinalize)
        # set same size for all buttons and controls
        width = 24
        for w in self._whistunzoom, self._whistzoomin, self._whistzoomout:
            w.setMinimumSize(width, width)
            w.setMaximumSize(width, width)
        self._whistzoom.setMinimumSize(80, width)
        self._wlab_histpos_text = "(hover here for help)"
        self._wlab_histpos = QLabel(self._wlab_histpos_text, self)
        self._wlab_histpos.setToolTip("""
      <P>The plot shows a histogram of either the full image or its selected subset
      (as per the "Data subset" section below).</P>
      <P>The current intensity range is indicated by the grey box
      in the plot.</P>
      <P>Use the left mouse button to change the low intensity limit, and the right
      button (on Macs, use Ctrl-click) to change the high limit.</P>
      <P>Use Shift with the left mouse button to zoom into an area of the histogram,
      or else use the "zoom wheel" control or the plus/minus toolbuttons above the histogram to zoom in or out.
      To zoom back out to the full extent of the histogram, click on the rightmost button above the histogram.</P>
      """)
        lo2.addWidget(self._wlab_histpos, 1)
        lo2.addWidget(self._wautozoom)
        lo2.addWidget(self._wlogy, 0)
        lo2.addWidget(self._whistzoomin, 0)
        lo2.addWidget(self._whistzoom, 0)
        lo2.addWidget(self._whistzoomout, 0)
        lo2.addWidget(self._whistunzoom, 0)
        self._zooming_histogram = False

        sliced_axes = rc.slicedAxes()
        dprint(1, "sliced axes are", sliced_axes)
        self._stokes_axis = None

        # subset indication
        lo0.addWidget(Separator(self, "Data subset"))
        # sliced axis selectors
        self._wslicers = []
        if sliced_axes:
            lo1 = QHBoxLayout()
            lo1.setContentsMargins(0, 0, 0, 0)
            lo1.setSpacing(2)
            lo0.addLayout(lo1)
            lo1.addWidget(QLabel("Current slice:  ", self))
            for i, (iextra, name, labels) in enumerate(sliced_axes):
                lo1.addWidget(QLabel("%s:" % name, self))
                if name == "STOKES":
                    self._stokes_axis = iextra
                # add controls
                wslicer = QComboBox(self)
                self._wslicers.append(wslicer)
                wslicer.addItems(labels)
                wslicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % name)
                wslicer.setCurrentIndex(self._rc.currentSlice()[iextra])
                QObject.connect(wslicer, SIGNAL("activated(int)"), self._currier.curry(self._rc.changeSlice, iextra))
                lo2 = QVBoxLayout()
                lo1.addLayout(lo2)
                lo2.setContentsMargins(0, 0, 0, 0)
                lo2.setSpacing(0)
                wminus = QToolButton(self)
                wminus.setArrowType(Qt.UpArrow)
                QObject.connect(wminus, SIGNAL("clicked()"), self._currier.curry(self._rc.incrementSlice, iextra, 1))
                if i == 0:
                    wminus.setShortcut(Qt.SHIFT + Qt.Key_F7)
                elif i == 1:
                    wminus.setShortcut(Qt.SHIFT + Qt.Key_F8)
                wplus = QToolButton(self)
                wplus.setArrowType(Qt.DownArrow)
                QObject.connect(wplus, SIGNAL("clicked()"), self._currier.curry(self._rc.incrementSlice, iextra, -1))
                if i == 0:
                    wplus.setShortcut(Qt.Key_F7)
                elif i == 1:
                    wplus.setShortcut(Qt.Key_F8)
                wminus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
                wplus.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
                sz = QSize(12, 8)
                wminus.setMinimumSize(sz)
                wplus.setMinimumSize(sz)
                wminus.resize(sz)
                wplus.resize(sz)
                lo2.addWidget(wminus)
                lo2.addWidget(wplus)
                lo1.addWidget(wslicer)
                lo1.addSpacing(5)
            lo1.addStretch(1)
        # subset indicator
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo1.setSpacing(2)
        lo0.addLayout(lo1)
        self._wlab_subset = QLabel("Subset: xxx", self)
        self._wlab_subset.setToolTip("""<P>This indicates the current data subset to which the histogram
      and the stats given here apply. Use the "Reset to" control on the right to change the
      current subset and recompute the histogram and stats.</P>""")
        lo1.addWidget(self._wlab_subset, 1)

        self._wreset_full = self.makeButton("\u2192 full", self._rc.setFullSubset)
        lo1.addWidget(self._wreset_full)
        if sliced_axes:
            #      if self._stokes_axis is not None and len(sliced_axes)>1:
            #        self._wreset_stokes = self.makeButton(u"\u21920Stokes",self._rc.setFullSubset)
            self._wreset_slice = self.makeButton("\u2192 slice", self._rc.setSliceSubset)
            lo1.addWidget(self._wreset_slice)
        else:
            self._wreset_slice = None

        # min/max controls
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 0)
        self._wlab_stats = QLabel(self)
        lo1.addWidget(self._wlab_stats, 0)
        self._wmore_stats = self.makeButton("more...", self._showMeanStd)
        self._wlab_stats.setMinimumHeight(self._wmore_stats.height())
        lo1.addWidget(self._wmore_stats, 0)
        lo1.addStretch(1)

        # intensity controls
        lo0.addWidget(Separator(self, "Intensity mapping"))
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo1.setSpacing(2)
        lo0.addLayout(lo1, 0)
        self._range_validator = FloatValidator(self)
        self._wrange = QLineEdit(self), QLineEdit(self)
        self._wrange[0].setToolTip("""<P>This is the low end of the intensity range.</P>""")
        self._wrange[1].setToolTip("""<P>This is the high end of the intensity range.</P>""")
        for w in self._wrange:
            w.setValidator(self._range_validator)
            QObject.connect(w, SIGNAL("editingFinished()"), self._changeDisplayRange)
        lo1.addWidget(QLabel("low:", self), 0)
        lo1.addWidget(self._wrange[0], 1)
        self._wrangeleft0 = self.makeButton("\u21920", self._setZeroLeftLimit, width=32)
        self._wrangeleft0.setToolTip("""<P>Click this to set the low end of the intensity range to 0.</P>""")
        lo1.addWidget(self._wrangeleft0, 0)
        lo1.addSpacing(8)
        lo1.addWidget(QLabel("high:", self), 0)
        lo1.addWidget(self._wrange[1], 1)
        lo1.addSpacing(8)
        self._wrange_full = self.makeButton(None, self._setHistDisplayRange, icon=pixmaps.intensity_graph.icon())
        lo1.addWidget(self._wrange_full)
        self._wrange_full.setToolTip(
            """<P>Click this to reset the intensity range to the current extent of the histogram plot.</P>""")
        # add menu for display range
        range_menu = QMenu(self)
        wrange_menu = QToolButton(self)
        wrange_menu.setText("Reset to")
        wrange_menu.setToolTip("""<P>Use this to reset the intensity range to various pre-defined settings.</P>""")
        lo1.addWidget(wrange_menu)
        self._qa_range_full = range_menu.addAction(pixmaps.full_range.icon(), "Full subset",
                                                   self._rc.resetSubsetDisplayRange)
        self._qa_range_hist = range_menu.addAction(pixmaps.intensity_graph.icon(), "Current histogram limits",
                                                   self._setHistDisplayRange)
        for percent in (99.99, 99.9, 99.5, 99, 98, 95):
            range_menu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent))
        wrange_menu.setMenu(range_menu)
        wrange_menu.setPopupMode(QToolButton.InstantPopup)

        lo1 = QGridLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 0)
        self._wimap = QComboBox(self)
        lo1.addWidget(QLabel("Intensity policy:", self), 0, 0)
        lo1.addWidget(self._wimap, 1, 0)
        self._wimap.addItems(rc.getIntensityMapNames())
        QObject.connect(self._wimap, SIGNAL("currentIndexChanged(int)"), self._rc.setIntensityMapNumber)
        self._wimap.setToolTip("""<P>Use this to change the type of the intensity transfer function (ITF).</P>""")

        # log cycles control
        lo1.setColumnStretch(1, 1)
        self._wlogcycles_label = QLabel("Log cycles: ", self)
        lo1.addWidget(self._wlogcycles_label, 0, 1)
        #    self._wlogcycles = QwtWheel(self)
        #    self._wlogcycles.setTotalAngle(360)
        self._wlogcycles = QwtSlider(self)
        self._wlogcycles.setToolTip(
            """<P>Use this to change the log-base for the logarithmic intensity transfer function (ITF).</P>""")
        # This works around a stupid bug in QwtSliders -- see comments on histogram zoom wheel above
        self._wlogcycles_timer = QTimer(self)
        self._wlogcycles_timer.setSingleShot(True)
        self._wlogcycles_timer.setInterval(500)
        QObject.connect(self._wlogcycles_timer, SIGNAL("timeout()"), self._setIntensityLogCycles)
        lo1.addWidget(self._wlogcycles, 1, 1)
        self._wlogcycles.setRange(1., 10)
        self._wlogcycles.setStep(0.1)
        self._wlogcycles.setTracking(False)
        QObject.connect(self._wlogcycles, SIGNAL("valueChanged(double)"), self._setIntensityLogCycles)
        QObject.connect(self._wlogcycles, SIGNAL("sliderMoved(double)"), self._previewIntensityLogCycles)
        self._updating_imap = False

        # lock intensity map
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 0)
        #    lo1.addWidget(QLabel("Lock range accross",self))
        wlock = QCheckBox("Lock display range", self)
        wlock.setToolTip("""<P>If checked, then the intensity range will be locked. The ranges of all locked images
      change simultaneously.</P>""")
        lo1.addWidget(wlock)
        wlockall = QToolButton(self)
        wlockall.setIcon(pixmaps.locked.icon())
        wlockall.setText("Lock all to this")
        wlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        wlockall.setAutoRaise(True)
        wlockall.setToolTip("""<P>Click this to lock together the intensity ranges of all images.</P>""")
        lo1.addWidget(wlockall)
        wunlockall = QToolButton(self)
        wunlockall.setIcon(pixmaps.unlocked.icon())
        wunlockall.setText("Unlock all")
        wunlockall.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        wunlockall.setAutoRaise(True)
        wunlockall.setToolTip("""<P>Click this to unlock the intensity ranges of all images.</P>""")
        lo1.addWidget(wunlockall)
        wlock.setChecked(self._rc.isDisplayRangeLocked())
        QObject.connect(wlock, SIGNAL("clicked(bool)"), self._rc.lockDisplayRange)
        QObject.connect(wlockall, SIGNAL("clicked()"),
                        self._currier.curry(self._imgman.lockAllDisplayRanges, self._rc))
        QObject.connect(wunlockall, SIGNAL("clicked()"), self._imgman.unlockAllDisplayRanges)
        QObject.connect(self._rc, SIGNAL("displayRangeLocked"), wlock.setChecked)

        #    self._wlock_imap_axis = [ QCheckBox(name,self) for iaxis,name,labels in sliced_axes ]
        #    for iw,w in enumerate(self._wlock_imap_axis):
        #      QObject.connect(w,SIGNAL("toggled(bool)"),self._currier.curry(self._rc.lockDisplayRangeForAxis,iw))
        #      lo1.addWidget(w,0)
        lo1.addStretch(1)

        # lo0.addWidget(Separator(self,"Colourmap"))
        # color bar
        self._colorbar = QwtPlot(self)
        lo0.addWidget(self._colorbar)
        self._colorbar.setAutoDelete(False)
        self._colorbar.setMinimumHeight(32)
        self._colorbar.enableAxis(QwtPlot.yLeft, False)
        self._colorbar.enableAxis(QwtPlot.xBottom, False)
        # color plot
        self._colorplot = QwtPlot(self)
        lo0.addWidget(self._colorplot)
        self._colorplot.setAutoDelete(False)
        self._colorplot.setMinimumHeight(64)
        self._colorplot.enableAxis(QwtPlot.yLeft, False)
        self._colorplot.enableAxis(QwtPlot.xBottom, False)
        # self._colorplot.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Preferred)
        self._colorbar.hide()
        self._colorplot.hide()
        # color controls
        lo1 = QHBoxLayout()
        lo1.setContentsMargins(0, 0, 0, 0)
        lo0.addLayout(lo1, 1)
        lo1.addWidget(QLabel("Colourmap:", self))
        # colormap list
        ### NB: use setIconSize() and icons in QComboBox!!!
        self._wcolmaps = QComboBox(self)
        self._wcolmaps.setIconSize(QSize(128, 16))
        self._wcolmaps.setToolTip("""<P>Use this to select a different colourmap.</P>""")
        for cmap in self._rc.getColormapList():
            self._wcolmaps.addItem(QIcon(cmap.makeQPixmap(128, 16)), cmap.name)
        lo1.addWidget(self._wcolmaps)
        QObject.connect(self._wcolmaps, SIGNAL("activated(int)"), self._rc.setColorMapNumber)
        # add widgetstack for colormap controls
        self._wcolmap_control_stack = QStackedWidget(self)
        self._wcolmap_control_blank = QWidget(self._wcolmap_control_stack)
        self._wcolmap_control_stack.addWidget(self._wcolmap_control_blank)
        lo0.addWidget(self._wcolmap_control_stack)
        self._colmap_controls = []
        # add controls to stack
        for index, cmap in enumerate(self._rc.getColormapList()):
            if isinstance(cmap, Colormaps.ColormapWithControls):
                controls = cmap.makeControlWidgets(self._wcolmap_control_stack)
                self._wcolmap_control_stack.addWidget(controls)
                QObject.connect(cmap, SIGNAL("colormapChanged"),
                                self._currier.curry(self._previewColormapParameters, index, cmap))
                QObject.connect(cmap, SIGNAL("colormapPreviewed"),
                                self._currier.curry(self._previewColormapParameters, index, cmap))
                self._colmap_controls.append(controls)
            else:
                self._colmap_controls.append(self._wcolmap_control_blank)

        # connect updates from renderControl and image
        self.image.connect(SIGNAL("slice"), self._updateImageSlice)
        QObject.connect(self._rc, SIGNAL("intensityMapChanged"), self._updateIntensityMap)
        QObject.connect(self._rc, SIGNAL("colorMapChanged"), self._updateColorMap)
        QObject.connect(self._rc, SIGNAL("dataSubsetChanged"), self._updateDataSubset)
        QObject.connect(self._rc, SIGNAL("displayRangeChanged"), self._updateDisplayRange)

        # update widgets
        self._setupHistogramPlot()
        self._updateDataSubset(*self._rc.currentSubset())
        self._updateColorMap(image.colorMap())
        self._updateIntensityMap(rc.currentIntensityMap(), rc.currentIntensityMapNumber())
        self._updateDisplayRange(*self._rc.displayRange())
Exemple #38
0
class ImageManager(QWidget):
    """An ImageManager manages a stack of images (and associated ImageControllers)"""
    def __init__(self, *args):
        QWidget.__init__(self, *args)
        # 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._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)
        QObject.connect(self._qa_plot_all, SIGNAL("toggled(bool)"),
                        self._displayAllImages)
        self._closing = False

        self._qa_load_clipboard = None
        self._clipboard_mode = QClipboard.Clipboard
        QObject.connect(QApplication.clipboard(),
                        SIGNAL("changed(QClipboard::Mode)"),
                        self._checkClipboardPath)
        # populate the menu
        self._repopulateMenu()

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

    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)
                QObject.connect(dialog,
                                SIGNAL("filesSelected(const QStringList &)"),
                                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.showErrorMessage("""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.showMessage("""Reading FITS image %s""" % filename, 3000)
        QApplication.flush()
        try:
            image = SkyImage.FITSImagePlotItem(str(filename))
        except KeyboardInterrupt:
            raise
        except:
            busy = None
            traceback.print_exc()
            self.showErrorMessage(
                """<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)
        self.showMessage("""Loaded FITS image %s""" % filename, 3000)
        dprint(2, "image loaded")
        return ic

    def showMessage(self, message, time=None):
        self.emit(SIGNAL("showMessage"), message, time)

    def showErrorMessage(self, message, time=None):
        self.emit(SIGNAL("showErrorMessage"), message, time)

    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):
        """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):
        """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))
        # 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.emit(SIGNAL("imagesChanged"))
        if self._imagecons:
            self.raiseImage(self._imagecons[0])

    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:
            self.emit(SIGNAL("imagesChanged"))

    def raiseImage(self, imagecon):
        # 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
            self.emit(SIGNAL("imageRaised"))
            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 signasl
        self.emit(SIGNAL("imageRaised"), img)

    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()

    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.showErrorMessage("""Error parsing expression "%s": %s.""" %
                                  (expression, str(exc)))
            return None
        # try to evaluate expression
        self.showMessage("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 = None
            traceback.print_exc()
            self.showErrorMessage("""Error evaluating "%s": %s.""" %
                                  (expression, str(exc)))
            return None
        busy = None
        if type(result) != numpy.ma.masked_array and type(
                result) != numpy.ndarray:
            self.showErrorMessage(
                """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.showErrorMessage(
                """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.showErrorMessage(
                """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.showMessage("""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 = None
            traceback.print_exc()
            self.showErrorMessage("""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.showMessage("Created new image for %s" % expression, 3000)
        dprint(2, "image created")

    def _createImageController(self,
                               image,
                               name,
                               basename,
                               model=False,
                               save=False):
        dprint(2, "creating ImageController for", name)
        ic = ImageController(image, self, self, name, save=save)
        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)
        # attach appropriate signals
        image.connect(SIGNAL("slice"), self.fastReplot)
        image.connect(SIGNAL("repaint"), self.replot)
        image.connect(SIGNAL("raise"),
                      self._currier.curry(self.raiseImage, ic))
        image.connect(SIGNAL("unload"),
                      self._currier.curry(self.unloadImage, ic))
        image.connect(SIGNAL("center"),
                      self._currier.curry(self.centerImage, ic))
        QObject.connect(
            ic.renderControl(), SIGNAL("displayRangeChanged"),
            self._currier.curry(self._updateDisplayRange, ic.renderControl()))
        QObject.connect(
            ic.renderControl(), SIGNAL("displayRangeLocked"),
            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.emit(SIGNAL("imagesChanged"))
        return ic
Exemple #39
0
class BooksView(QTableView):  # {{{

    files_dropped = pyqtSignal(object)
    add_column_signal = pyqtSignal()

    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.setProperty('highlight_current_item', 150)
        self.row_sizing_done = False

        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())

        self.drag_allowed = True
        self.setDragEnabled(True)
        self.setDragDropOverwriteMode(False)
        self.setDragDropMode(self.DragDrop)
        self.drag_start_pos = None
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(self.SelectRows)
        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)

        # {{{ 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.setMovable(True)
        self.column_header.setClickable(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.setVerticalHeader(self.row_header)
        # }}}

        self._model.database_changed.connect(self.database_changed)
        hv = self.verticalHeader()
        hv.setClickable(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':
            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.sortByColumn(idx, Qt.AscendingOrder)
        elif action == 'descending':
            self.sortByColumn(idx, Qt.DescendingOrder)
        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).toString())
            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).toString())
                    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 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)
            if order:
                self.sortByColumn(idx, Qt.AscendingOrder)
            else:
                self.sortByColumn(idx, Qt.DescendingOrder)
        else:
            self._model.sort_by_named_field(field, order, reset)

    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)
            hdrs = self.horizontalHeader()
            try:
                hdrs.setSortIndicator(col, dir)
            except:
                pass

    # }}}

    # Ondevice column {{{
    def set_ondevice_column_visibility(self):
        m = self._model
        self.column_header.setSectionHidden(m.column_map.index('ondevice'),
                                            not m.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)
        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.prefs.set(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
            if col == 'date':
                col = 'timestamp'
            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
        for col, order in reversed(
                self.cleanup_sort_history(saved_history)[:max_sort_levels]):
            self.sortByColumn(
                self.column_map.index(col),
                Qt.AscendingOrder if order else Qt.DescendingOrder)

    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)

        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.prefs[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.prefs[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:
            self.resizeRowToContents(0)
            self.verticalHeader().setDefaultSectionSize(
                self.rowHeight(0) + gprefs['extra_row_spacing'])
            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.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)

    def database_changed(self, db):
        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()
        #}}}

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

    def contextMenuEvent(self, event):
        self.context_menu.popup(event.globalPos())
        event.accept()

    # }}}

    # Drag 'n Drop {{{
    @classmethod
    def paths_from_event(cls, event):
        '''
        Accept a drop event and return a list of paths that can be read from
        and represent files with extensions.
        '''
        md = event.mimeData()
        if md.hasFormat('text/uri-list') and not \
                md.hasFormat('application/calibre+from_library'):
            urls = [unicode(u.toLocalFile()) for u in md.urls()]
            return [
                u for u in urls if os.path.splitext(u)[1] and os.path.exists(u)
            ]

    def drag_icon(self, cover, multiple):
        cover = cover.scaledToHeight(120, Qt.SmoothTransformation)
        if multiple:
            base_width = cover.width()
            base_height = cover.height()
            base = QImage(base_width + 21, base_height + 21,
                          QImage.Format_ARGB32_Premultiplied)
            base.fill(QColor(255, 255, 255, 0).rgba())
            p = QPainter(base)
            rect = QRect(20, 0, base_width, base_height)
            p.fillRect(rect, QColor('white'))
            p.drawRect(rect)
            rect.moveLeft(10)
            rect.moveTop(10)
            p.fillRect(rect, QColor('white'))
            p.drawRect(rect)
            rect.moveLeft(0)
            rect.moveTop(20)
            p.fillRect(rect, QColor('white'))
            p.save()
            p.setCompositionMode(p.CompositionMode_SourceAtop)
            p.drawImage(rect.topLeft(), cover)
            p.restore()
            p.drawRect(rect)
            p.end()
            cover = base
        return QPixmap.fromImage(cover)

    def drag_data(self):
        m = self.model()
        db = m.db
        rows = self.selectionModel().selectedRows()
        selected = list(map(m.id, rows))
        ids = ' '.join(map(str, selected))
        md = QMimeData()
        md.setData('application/calibre+from_library', ids)
        fmt = prefs['output_format']

        def url_for_id(i):
            try:
                ans = db.format_path(i, fmt, index_is_id=True)
            except:
                ans = None
            if ans is None:
                fmts = db.formats(i, index_is_id=True)
                if fmts:
                    fmts = fmts.split(',')
                else:
                    fmts = []
                for f in fmts:
                    try:
                        ans = db.format_path(i, f, index_is_id=True)
                    except:
                        ans = None
            if ans is None:
                ans = db.abspath(i, index_is_id=True)
            return QUrl.fromLocalFile(ans)

        md.setUrls([url_for_id(i) for i in selected])
        drag = QDrag(self)
        col = self.selectionModel().currentIndex().column()
        md.column_name = self.column_map[col]
        drag.setMimeData(md)
        cover = self.drag_icon(m.cover(self.currentIndex().row()),
                               len(selected) > 1)
        drag.setHotSpot(QPoint(-15, -15))
        drag.setPixmap(cover)
        return drag

    def event_has_mods(self, event=None):
        mods = event.modifiers() if event is not None else \
                QApplication.keyboardModifiers()
        return mods & Qt.ControlModifier or mods & Qt.ShiftModifier

    def mousePressEvent(self, event):
        ep = event.pos()
        if self.indexAt(ep) in self.selectionModel().selectedIndexes() and \
                event.button() == Qt.LeftButton and not self.event_has_mods():
            self.drag_start_pos = ep
        return QTableView.mousePressEvent(self, event)

    def mouseMoveEvent(self, event):
        if not self.drag_allowed:
            return
        if self.drag_start_pos is None:
            return QTableView.mouseMoveEvent(self, event)

        if self.event_has_mods():
            self.drag_start_pos = None
            return

        if not (event.buttons() & Qt.LeftButton) or \
                (event.pos() - self.drag_start_pos).manhattanLength() \
                      < QApplication.startDragDistance():
            return

        index = self.indexAt(event.pos())
        if not index.isValid():
            return
        drag = self.drag_data()
        drag.exec_(Qt.CopyAction)
        self.drag_start_pos = None

    def dragEnterEvent(self, event):
        if int(event.possibleActions() & Qt.CopyAction) + \
           int(event.possibleActions() & Qt.MoveAction) == 0:
            return
        paths = self.paths_from_event(event)

        if paths:
            event.acceptProposedAction()

    def dragMoveEvent(self, event):
        event.acceptProposedAction()

    def dropEvent(self, event):
        paths = self.paths_from_event(event)
        event.setDropAction(Qt.CopyAction)
        event.accept()
        self.files_dropped.emit(paths)

    # }}}

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

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

    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

    def set_current_row(self, row=0, select=True):
        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)
            self.setCurrentIndex(index)
            if select:
                sm = self.selectionModel()
                sm.select(index, sm.ClearAndSelect | sm.Rows)

    def keyPressEvent(self, ev):
        val = self.horizontalScrollBar().value()
        ret = super(BooksView, self).keyPressEvent(ev)
        if ev.isAccepted() and ev.key() in (
                Qt.Key_Home,
                Qt.Key_End) and ev.modifiers() & Qt.ControlModifier:
            self.horizontalScrollBar().setValue(val)
        return ret

    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)
Exemple #40
0
def makemenu_helper(widget, menu_spec, menu=None):
    """
    Make and return a reusable or one-time-use (at caller's option)
    popup menu whose structure is specified by menu_spec,
    which is a list of menu item specifiers, each of which is either None
    (for a separator) or a tuple of the form (menu text, callable or submenu,
    option1, option2, ...) with 0 or more options (described below).
       A submenu can be either another menu_spec list, or a QMenu object
    (but in the latter case the menu text is ignored -- maybe it comes
    from that QMenu object somehow -- not sure if this was different in Qt3).
    In either case it is the 2nd menu-item-tuple element, in place of the callable.
       Otherwise the callable must satisfy the python 'callable' predicate,
    and is executed if the menu item is chosen, wrapped inside another function
    which handles Undo checkpointing and Undo-command-name setting.
       The options in a menu item tuple can be zero or more (in any order,
    duplicates allowed) of the following:
    'disabled' -- the menu item should be disabled;
    'checked' -- the menu item will be checked;
    None -- this option is legal but ignored (but the callable must still satisfy
    the python predicate "callable"; constants.noop might be useful for that case).
       The Qt3 version also supported tuple-options consisting of one of the words
    'iconset' and 'whatsThis' followed by an appropriate argument, but those have
    not yet been ported to Qt4 (except possibly for disabled menu items -- UNTESTED).
       Unrecognized options may or may not generate warnings, and are otherwise ignored.
    [###FIX that -- they always ought to print a warning to developers. Note that right
    now they do iff 'disabled' is one of the options and ATOM_DEBUG is set.]
       The 'widget' argument should be the Qt widget
    which is using this function to put up a menu.
       If the menu argument is provided, it should be a QMenu
    to which we'll add items; otherwise we create our own QMenu
    and add items to it.
    """
    from utilities.debug import print_compact_traceback
    import types
    if menu is None:
        menu = QMenu(widget)
        ## menu.show()
        #bruce 070514 removed menu.show() to fix a cosmetic and performance bug
        # (on Mac, possibly on other platforms too; probably unreported)
        # in which the debug menu first appears in screen center, slowly grows
        # to full size while remaining blank, then moves to its final position
        # and looks normal (causing a visual glitch, and a 2-3 second delay
        # in being able to use it). May fix similar issues with other menus.
        # If this causes harm for some menus or platforms, we can adapt it.
    # bruce 040909-16 moved this method from basicMode to GLPane,
    # leaving a delegator for it in basicMode.
    # (bruce was not the original author, but modified it)
    #menu = QMenu( widget)
    for m in menu_spec:
        try:  #bruce 050416 added try/except as debug code and for safety
            menutext = m and widget.trUtf8(m[0])
            if m and isinstance(m[1], QMenu):  #bruce 041010 added this case
                submenu = m[1]
                #menu.insertItem( menutext, submenu )
                menu.addMenu(submenu)  # how do I get menutext in there?
                # (similar code might work for QAction case too, not sure)
            elif m and isinstance(
                    m[1], types.ListType):  #bruce 041103 added this case
                submenu = QMenu(menutext, menu)
                submenu = makemenu_helper(
                    widget, m[1],
                    submenu)  # [this used to call widget.makemenu]
                menu.addMenu(submenu)
            elif m:
                assert callable(m[1]), \
                    "%r[1] needs to be a callable" % (m,) #bruce 041103
                # transform m[1] into a new callable that makes undo checkpoints and provides an undo command-name
                # [bruce 060324 for possible bugs in undo noticing cmenu items, and for the cmdnames]
                func = wrap_callable_for_undo(m[1], cmdname=m[0])
                # guess about cmdname, but it might be reasonable for A7 as long as we ensure weird characters won't confuse it
                import foundation.changes as changes
                changes.keep_forever(
                    func
                )  # THIS IS BAD (memory leak), but it's not a severe one, so ok for A7 [bruce 060324]
                # (note: the hard part about removing these when we no longer need them is knowing when to do that
                #  if the user ends up not selecting anything from the menu. Also, some callers make these
                #  menus for reuse multiple times, and for them we never want to deallocate func even when some
                #  menu command gets used. We could solve both of these by making the caller pass a place to keep these
                #  which it would deallocate someday or which would ensure only one per distinct kind of menu is kept. #e)
                if 'disabled' not in m[2:]:
                    act = QAction(widget)
                    act.setText(menutext)
                    if 'checked' in m[2:]:
                        act.setCheckable(True)
                        act.setChecked(True)
                    menu.addAction(act)
                    widget.connect(act, SIGNAL("activated()"), func)
                else:
                    # disabled case
                    # [why is this case done differently, in this Qt4 port?? -- bruce 070522 question]
                    insert_command_into_menu(menu,
                                             menutext,
                                             func,
                                             options=m[2:],
                                             raw_command=True)
            else:
                menu.addSeparator(
                )  #bruce 070522 bugfix -- before this, separators were placed lower down or dropped
                # so as not to come before disabled items, for unknown reasons.
                # (Speculation: maybe because insertSeparator was used, since addSeparator didn't work or wasn't noticed,
                #  and since disabled item were added by an older function (also for unknown reasons)?)
                pass
        except Exception, e:
            if isinstance(e, SystemExit):
                raise
            print_compact_traceback(
                "exception in makemenu_helper ignored, for %r:\n" % (m, ))
            #bruce 070522 restored this (was skipped in Qt4 port)
            pass  #e could add a fake menu item here as an error message
class Ui_ProteinSequenceEditor(PM_DockWidget):
    """
    The Ui_DnaSequenceEditor class defines UI elements for the Sequence Editor
    object. The sequence editor is usually visible while in DNA edit mode.
    It is a DockWidget that is doced at the bottom of the MainWindow
    """
    _title         =  "Sequence Editor"
    _groupBoxCount = 0
    _lastGroupBox = None

    def __init__(self, win):
        """
        Constructor for the Ui_DnaSequenceEditor 
        @param win: The parentWidget (MainWindow) for the sequence editor 
        """
        
        self.win = win
        # Should parentWidget for a docwidget always be win? 
        #Not necessary but most likely it will be the case.        
        parentWidget = win 
        
        _superclass.__init__(self, parentWidget, title = self._title)
        
        #A flag used to restore the state of the Reports dock widget 
        #(which can be accessed through View  >  Reports) see self.show() and
        #self.closeEvent() for more details. 
        self._reportsDockWidget_closed_in_show_method = False
        self.setFixedHeight(90)

    def show(self):
        """
        Shows the sequence editor. While doing this, it also closes the reports
        dock widget (if visible) the state of the reports dockwidget will be
        restored when the sequence editor is closed. 
        @see:self.closeEvent()
        """
        self._reportsDockWidget_closed_in_show_method = False
        
        if self.win.viewFullScreenAction.isChecked() or \
           self.win.viewSemiFullScreenAction.isChecked():
            pass
        else:
            if self.win.reportsDockWidget.isVisible():
                self.win.reportsDockWidget.close()
                self._reportsDockWidget_closed_in_show_method = True

        _superclass.show(self)  
        
    def closeEvent(self, event):
        """
        Overrides close event. Makes sure that the visible state of the reports
        widgetis restored when the sequence editor is closed. 
        @see: self.show()
        """
        _superclass.closeEvent(self, event)
       
        if self.win.viewFullScreenAction.isChecked() or \
           self.win.viewSemiFullScreenAction.isChecked():
            pass
        else:
            if self._reportsDockWidget_closed_in_show_method:
                self.win.viewReportsAction.setChecked(True) 
                self._reportsDockWidget_closed_in_show_method = False

    def _loadWidgets(self):
        """
        Overrides PM.PM_DockWidget._loadWidgets. Loads the widget in this
        dockwidget.
        """
        self._loadMenuWidgets()
        self._loadTextEditWidget()


    def _loadMenuWidgets(self):
        """
        Load the various menu widgets (e.g. Open, save sequence options, 
        Find and replace widgets etc. 
        """
        #Note: Find and replace widgets might be moved to their own class.

        self.loadSequenceButton = PM_ToolButton(
            self,
            iconPath = "ui/actions/Properties Manager/Open.png")  

        self.saveSequenceButton = PM_ToolButton(
            self, 
            iconPath = "ui/actions/Properties Manager/Save_Strand_Sequence.png") 

        self.loadSequenceButton.setAutoRaise(True)
        self.saveSequenceButton.setAutoRaise(True)

        
        #Find and replace widgets --
        self.findLineEdit = \
            PM_LineEdit( self, 
                         label        = "",
                         spanWidth    = False)
        self.findLineEdit.setMaximumWidth(60)


        self.replaceLineEdit = \
            PM_LineEdit( self, 
                         label        = "",
                         spanWidth    = False)
        self.replaceLineEdit.setMaximumWidth(60)

        self.findOptionsToolButton = PM_ToolButton(self)
        self.findOptionsToolButton.setMaximumWidth(12)
        self.findOptionsToolButton.setAutoRaise(True)

        self.findOptionsToolButton.setPopupMode(QToolButton.MenuButtonPopup)

        self._setFindOptionsToolButtonMenu()

        self.findNextToolButton = PM_ToolButton(
            self,
            iconPath = "ui/actions/Properties Manager/Find_Next.png")
        self.findNextToolButton.setAutoRaise(True)

        self.findPreviousToolButton = PM_ToolButton(
            self,
            iconPath = "ui/actions/Properties Manager/Find_Previous.png")
        self.findPreviousToolButton.setAutoRaise(True)

        self.replacePushButton = PM_PushButton(self, text = "Replace")

        self.warningSign = QLabel(self)
        self.warningSign.setPixmap(
            getpixmap('ui/actions/Properties Manager/Warning.png'))
        self.warningSign.hide()

        self.phraseNotFoundLabel = QLabel(self)
        self.phraseNotFoundLabel.setText("Sequence Not Found")
        self.phraseNotFoundLabel.hide()

        

        #Widgets to include in the widget row. 
        widgetList = [('PM_ToolButton', self.loadSequenceButton, 0),
                      ('PM_ToolButton', self.saveSequenceButton, 1),
                      ('QLabel', "     Find:", 4),
                      ('PM_LineEdit', self.findLineEdit, 5),
                      ('PM_ToolButton', self.findOptionsToolButton, 6),
                      ('PM_ToolButton', self.findPreviousToolButton, 7),
                      ('PM_ToolButton', self.findNextToolButton, 8), 
                      ('QLabel', "     Replace:", 9),
                      ('PM_TextEdit', self.replaceLineEdit, 10), 
                      ('PM_PushButton', self.replacePushButton, 11),
                      ('PM_Label', self.warningSign, 12),
                      ('PM_Label', self.phraseNotFoundLabel, 13),
                      ('QSpacerItem', 5, 5, 14) ]

        widgetRow = PM_WidgetRow(self,
                                 title     = '',
                                 widgetList = widgetList,
                                 label = "",
                                 spanWidth = True )
        
        

    def _loadTextEditWidget(self):
        """
        Load the SequenceTexteditWidgets.         
        """        
        self.aaRulerTextEdit = \
            PM_TextEdit( self, 
                         label = "", 
                         spanWidth = False,
                         permit_enter_keystroke = False) 
        
        palette = getPalette(None, 
                             QPalette.Base, 
                             pmGrpBoxColor)
        self.aaRulerTextEdit.setPalette(palette)     
        self.aaRulerTextEdit.setWordWrapMode( QTextOption.WrapAnywhere )
        self.aaRulerTextEdit.setFixedHeight(20)
        self.aaRulerTextEdit.setReadOnly(True)
        
        self.sequenceTextEdit = \
            PM_TextEdit( self, 
                         label = " Sequence: ", 
                         spanWidth = False,
                         permit_enter_keystroke = False) 
        
        
        self.sequenceTextEdit.setCursorWidth(2)
        self.sequenceTextEdit.setWordWrapMode( QTextOption.WrapAnywhere )
        self.sequenceTextEdit.setFixedHeight(20)
        
        self.secStrucTextEdit = \
            PM_TextEdit( self, 
                         label = " Secondary structure: ", 
                         spanWidth = False,
                         permit_enter_keystroke = False) 
        
        palette = getPalette(None, 
                             QPalette.Base, 
                             sequenceEditStrandMateBaseColor)
        self.secStrucTextEdit.setPalette(palette)     
        self.secStrucTextEdit.setWordWrapMode( QTextOption.WrapAnywhere )
        self.secStrucTextEdit.setFixedHeight(20)
        self.secStrucTextEdit.setReadOnly(True)

        #Important to make sure that the horizontal and vertical scrollbars 
        #for these text edits are never displayed. 
        
        self.sequenceTextEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.sequenceTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.secStrucTextEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.secStrucTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.aaRulerTextEdit.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.aaRulerTextEdit.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

    def _getFindLineEditStyleSheet(self):
        """
        Return the style sheet for the findLineEdit. This sets the following 
        properties only:
         - background-color

        This style is set whenever the searchStrig can't be found (sets
        a light red color background to the lineedit when this happens)   

        @return: The line edit style sheet.
        @rtype:  str

        """
        styleSheet = \
                   "QLineEdit {\
                   background-color: rgb(255, 102, 102)\
                   }"
        #Not used:
        #  background-color: rgb(217, 255, 216)\       

        return styleSheet

    def _setFindOptionsToolButtonMenu(self):
        """
        Sets the menu for the findOptionstoolbutton that appears a small 
        menu button next to the findLineEdit.
        """
        self.findOptionsMenu = QMenu(self.findOptionsToolButton)

        self.caseSensitiveFindAction = QAction(self.findOptionsToolButton)
        self.caseSensitiveFindAction.setText('Match Case')
        self.caseSensitiveFindAction.setCheckable(True)
        self.caseSensitiveFindAction.setChecked(False)

        self.findOptionsMenu.addAction(self.caseSensitiveFindAction)
        self.findOptionsMenu.addSeparator()

        self.findOptionsToolButton.setMenu(self.findOptionsMenu)

    def _addToolTipText(self):
        """
            What's Tool Tip text for widgets in this Property Manager.  
            """ 
        pass

    def _addWhatsThisText(self):
        """
            What's This text for widgets in this Property Manager.  

            """
        pass
Exemple #42
0
class Main(
        MainWindow,
        MainWindowMixin,
        DeviceMixin,
        EmailMixin,  # {{{
        TagBrowserMixin,
        CoverFlowMixin,
        LibraryViewMixin,
        SearchBoxMixin,
        SavedSearchBoxMixin,
        SearchRestrictionMixin,
        LayoutMixin,
        UpdateMixin,
        EbookDownloadMixin):
    'The main GUI'

    proceed_requested = pyqtSignal(object, object)

    def __init__(self, opts, parent=None, gui_debug=None):
        global _gui
        MainWindow.__init__(self,
                            opts,
                            parent=parent,
                            disable_automatic_gc=True)
        self.jobs_pointer = Pointer(self)
        self.proceed_requested.connect(self.do_proceed,
                                       type=Qt.QueuedConnection)
        self.proceed_question = ProceedQuestion(self)
        self.job_error_dialog = JobError(self)
        self.keyboard = Manager(self)
        _gui = self
        self.opts = opts
        self.device_connected = None
        self.gui_debug = gui_debug
        self.iactions = OrderedDict()
        # Actions
        for action in interface_actions():
            if opts.ignore_plugins and action.plugin_path is not None:
                continue
            try:
                ac = self.init_iaction(action)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if action.plugin_path is None:
                    raise
                continue
            ac.plugin_path = action.plugin_path
            ac.interface_action_base_plugin = action
            self.add_iaction(ac)
        self.load_store_plugins()

    def init_iaction(self, action):
        ac = action.load_actual_plugin(self)
        ac.plugin_path = action.plugin_path
        ac.interface_action_base_plugin = action
        action.actual_iaction_plugin_loaded = True
        return ac

    def add_iaction(self, ac):
        acmap = self.iactions
        if ac.name in acmap:
            if ac.priority >= acmap[ac.name].priority:
                acmap[ac.name] = ac
        else:
            acmap[ac.name] = ac

    def load_store_plugins(self):
        from calibre.gui2.store.loader import Stores
        self.istores = Stores()
        for store in available_store_plugins():
            if self.opts.ignore_plugins and store.plugin_path is not None:
                continue
            try:
                st = self.init_istore(store)
                self.add_istore(st)
            except:
                # Ignore errors in loading user supplied plugins
                import traceback
                traceback.print_exc()
                if store.plugin_path is None:
                    raise
                continue
        self.istores.builtins_loaded()

    def init_istore(self, store):
        st = store.load_actual_plugin(self)
        st.plugin_path = store.plugin_path
        st.base_plugin = store
        store.actual_istore_plugin_loaded = True
        return st

    def add_istore(self, st):
        stmap = self.istores
        if st.name in stmap:
            if st.priority >= stmap[st.name].priority:
                stmap[st.name] = st
        else:
            stmap[st.name] = st

    def initialize(self, library_path, db, listener, actions, show_gui=True):
        opts = self.opts
        self.preferences_action, self.quit_action = actions
        self.library_path = library_path
        self.content_server = None
        self.spare_servers = []
        self.must_restart_before_config = False
        self.listener = Listener(listener)
        self.check_messages_timer = QTimer()
        self.connect(self.check_messages_timer, SIGNAL('timeout()'),
                     self.another_instance_wants_to_talk)
        self.check_messages_timer.start(1000)

        for ac in self.iactions.values():
            try:
                ac.do_genesis()
            except Exception:
                # Ignore errors in third party plugins
                import traceback
                traceback.print_exc()
                if getattr(ac, 'plugin_path', None) is None:
                    raise
        self.donate_action = QAction(QIcon(I('donate.png')),
                                     _('&Donate to support calibre'), self)
        for st in self.istores.values():
            st.do_genesis()
        MainWindowMixin.__init__(self, db)

        # Jobs Button {{{
        self.job_manager = JobManager()
        self.jobs_dialog = JobsDialog(self, self.job_manager)
        self.jobs_button = JobsButton(horizontal=True, parent=self)
        self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
        # }}}

        LayoutMixin.__init__(self)
        EmailMixin.__init__(self)
        EbookDownloadMixin.__init__(self)
        DeviceMixin.__init__(self)

        self.progress_indicator = ProgressIndicator(self)
        self.progress_indicator.pos = (0, 20)
        self.verbose = opts.verbose
        self.get_metadata = GetMetadata()
        self.upload_memory = {}
        self.metadata_dialogs = []
        self.default_thumbnail = None
        self.tb_wrapper = textwrap.TextWrapper(width=40)
        self.viewers = collections.deque()
        self.system_tray_icon = SystemTrayIcon(QIcon(I('lt.png')), self)
        self.system_tray_icon.setToolTip('calibre')
        self.system_tray_icon.tooltip_requested.connect(
            self.job_manager.show_tooltip)
        if not config['systray_icon']:
            self.system_tray_icon.hide()
        else:
            self.system_tray_icon.show()
        self.system_tray_menu = QMenu(self)
        self.restore_action = self.system_tray_menu.addAction(
            QIcon(I('page.png')), _('&Restore'))
        self.system_tray_menu.addAction(self.donate_action)
        self.donate_button.setDefaultAction(self.donate_action)
        self.donate_button.setStatusTip(self.donate_button.toolTip())
        self.eject_action = self.system_tray_menu.addAction(
            QIcon(I('eject.png')), _('&Eject connected device'))
        self.eject_action.setEnabled(False)
        self.addAction(self.quit_action)
        self.system_tray_menu.addAction(self.quit_action)
        self.keyboard.register_shortcut('quit calibre',
                                        _('Quit calibre'),
                                        default_keys=('Ctrl+Q', ),
                                        action=self.quit_action)
        self.system_tray_icon.setContextMenu(self.system_tray_menu)
        self.connect(self.quit_action, SIGNAL('triggered(bool)'), self.quit)
        self.connect(self.donate_action, SIGNAL('triggered(bool)'),
                     self.donate)
        self.connect(self.restore_action, SIGNAL('triggered()'),
                     self.show_windows)
        self.system_tray_icon.activated.connect(
            self.system_tray_icon_activated)

        self.esc_action = QAction(self)
        self.addAction(self.esc_action)
        self.keyboard.register_shortcut('clear current search',
                                        _('Clear the current search'),
                                        default_keys=('Esc', ),
                                        action=self.esc_action)
        self.esc_action.triggered.connect(self.esc)

        self.shift_esc_action = QAction(self)
        self.addAction(self.shift_esc_action)
        self.keyboard.register_shortcut('focus book list',
                                        _('Focus the book list'),
                                        default_keys=('Shift+Esc', ),
                                        action=self.shift_esc_action)
        self.shift_esc_action.triggered.connect(self.shift_esc)

        self.ctrl_esc_action = QAction(self)
        self.addAction(self.ctrl_esc_action)
        self.keyboard.register_shortcut('clear virtual library',
                                        _('Clear the virtual library'),
                                        default_keys=('Ctrl+Esc', ),
                                        action=self.ctrl_esc_action)
        self.ctrl_esc_action.triggered.connect(self.ctrl_esc)

        self.alt_esc_action = QAction(self)
        self.addAction(self.alt_esc_action)
        self.keyboard.register_shortcut('clear additional restriction',
                                        _('Clear the additional restriction'),
                                        default_keys=('Alt+Esc', ),
                                        action=self.alt_esc_action)
        self.alt_esc_action.triggered.connect(
            self.clear_additional_restriction)

        ####################### Start spare job server ########################
        QTimer.singleShot(1000, self.add_spare_server)

        ####################### Location Manager ########################
        self.location_manager.location_selected.connect(self.location_selected)
        self.location_manager.unmount_device.connect(
            self.device_manager.umount_device)
        self.location_manager.configure_device.connect(
            self.configure_connected_device)
        self.location_manager.update_device_metadata.connect(
            self.update_metadata_on_device)
        self.eject_action.triggered.connect(self.device_manager.umount_device)

        #################### Update notification ###################
        UpdateMixin.__init__(self, opts)

        ####################### Search boxes ########################
        SearchRestrictionMixin.__init__(self)
        SavedSearchBoxMixin.__init__(self)

        ####################### Library view ########################
        LibraryViewMixin.__init__(self, db)
        SearchBoxMixin.__init__(self)  # Requires current_db

        if show_gui:
            self.show()

        if self.system_tray_icon.isVisible() and opts.start_in_tray:
            self.hide_windows()
        self.library_view.model().count_changed_signal.connect(
            self.iactions['Choose Library'].count_changed)
        if not gprefs.get('quick_start_guide_added', False):
            from calibre.ebooks.metadata.meta import get_metadata
            mi = get_metadata(open(P('quick_start.epub'), 'rb'), 'epub')
            self.library_view.model().add_books([P('quick_start.epub')],
                                                ['epub'], [mi])
            gprefs['quick_start_guide_added'] = True
            self.library_view.model().books_added(1)
            if hasattr(self, 'db_images'):
                self.db_images.reset()
            if self.library_view.model().rowCount(None) < 3:
                self.library_view.resizeColumnsToContents()

        for view in ('library', 'memory', 'card_a', 'card_b'):
            v = getattr(self, '%s_view' % view)
            v.selectionModel().selectionChanged.connect(self.update_status_bar)
            v.model().count_changed_signal.connect(self.update_status_bar)

        self.library_view.model().count_changed()
        self.bars_manager.database_changed(self.library_view.model().db)
        self.library_view.model().database_changed.connect(
            self.bars_manager.database_changed, type=Qt.QueuedConnection)

        ########################### Tags Browser ##############################
        TagBrowserMixin.__init__(self, db)

        ######################### Search Restriction ##########################
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()

        ########################### Cover Flow ################################

        CoverFlowMixin.__init__(self)

        self._calculated_available_height = min(max_available_height() - 15,
                                                self.height())
        self.resize(self.width(), self._calculated_available_height)

        self.build_context_menus()

        for ac in self.iactions.values():
            try:
                ac.gui_layout_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise

        if config['autolaunch_server']:
            self.start_content_server()

        self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)

        self.read_settings()
        self.finalize_layout()
        if self.bars_manager.showing_donate:
            self.donate_button.start_animation()
        self.set_window_title()

        for ac in self.iactions.values():
            try:
                ac.initialization_complete()
            except:
                import traceback
                traceback.print_exc()
                if ac.plugin_path is None:
                    raise
        self.device_manager.set_current_library_uuid(db.library_id)

        self.keyboard.finalize()
        self.auto_adder = AutoAdder(gprefs['auto_add_path'], self)

        self.save_layout_state()

        # Collect cycles now
        gc.collect()

        if show_gui and self.gui_debug is not None:
            info_dialog(
                self,
                _('Debug mode'),
                '<p>' +
                _('You have started calibre in debug mode. After you '
                  'quit calibre, the debug log will be available in '
                  'the file: %s<p>The '
                  'log will be displayed automatically.') % self.gui_debug,
                show=True)

        self.iactions['Connect Share'].check_smartdevice_menus()
        QTimer.singleShot(1, self.start_smartdevice)

    def esc(self, *args):
        self.clear_button.click()

    def shift_esc(self):
        self.current_view().setFocus(Qt.OtherFocusReason)

    def ctrl_esc(self):
        self.apply_virtual_library()
        self.current_view().setFocus(Qt.OtherFocusReason)

    def start_smartdevice(self):
        message = None
        if self.device_manager.get_option('smartdevice', 'autostart'):
            try:
                message = self.device_manager.start_plugin('smartdevice')
            except:
                message = 'start smartdevice unknown exception'
                prints(message)
                import traceback
                traceback.print_exc()
        if message:
            if not self.device_manager.is_running('Wireless Devices'):
                error_dialog(self,
                             _('Problem starting the wireless device'),
                             _('The wireless device driver did not start. '
                               'It said "%s"') % message,
                             show=True)
        self.iactions['Connect Share'].set_smartdevice_action_state()

    def start_content_server(self, check_started=True):
        from calibre.library.server.main import start_threaded_server
        from calibre.library.server import server_config
        self.content_server = start_threaded_server(
            self.library_view.model().db,
            server_config().parse())
        self.content_server.state_callback = Dispatcher(
            self.iactions['Connect Share'].content_server_state_changed)
        if check_started:
            self.content_server.start_failure_callback = \
                Dispatcher(self.content_server_start_failed)

    def content_server_start_failed(self, msg):
        error_dialog(self,
                     _('Failed to start Content Server'),
                     _('Could not start the content server. Error:\n\n%s') %
                     msg,
                     show=True)

    def resizeEvent(self, ev):
        MainWindow.resizeEvent(self, ev)
        self.search.setMaximumWidth(self.width() - 150)

    def add_spare_server(self, *args):
        self.spare_servers.append(
            Server(limit=int(config['worker_limit'] / 2.0)))

    @property
    def spare_server(self):
        # Because of the use of the property decorator, we're called one
        # extra time. Ignore.
        if not hasattr(self, '__spare_server_property_limiter'):
            self.__spare_server_property_limiter = True
            return None
        try:
            QTimer.singleShot(1000, self.add_spare_server)
            return self.spare_servers.pop()
        except:
            pass

    def do_proceed(self, func, payload):
        if callable(func):
            func(payload)

    def no_op(self, *args):
        pass

    def system_tray_icon_activated(self, r):
        if r == QSystemTrayIcon.Trigger:
            if self.isVisible():
                self.hide_windows()
            else:
                self.show_windows()

    @property
    def is_minimized_to_tray(self):
        return getattr(self, '__systray_minimized', False)

    def ask_a_yes_no_question(self,
                              title,
                              msg,
                              det_msg='',
                              show_copy_button=False,
                              ans_when_user_unavailable=True,
                              skip_dialog_name=None,
                              skipped_value=True):
        if self.is_minimized_to_tray:
            return ans_when_user_unavailable
        return question_dialog(self,
                               title,
                               msg,
                               det_msg=det_msg,
                               show_copy_button=show_copy_button,
                               skip_dialog_name=skip_dialog_name,
                               skip_dialog_skipped_value=skipped_value)

    def hide_windows(self):
        for window in QApplication.topLevelWidgets():
            if isinstance(window, (MainWindow, QDialog)) and \
                    window.isVisible():
                window.hide()
                setattr(window, '__systray_minimized', True)

    def show_windows(self):
        for window in QApplication.topLevelWidgets():
            if getattr(window, '__systray_minimized', False):
                window.show()
                setattr(window, '__systray_minimized', False)

    def test_server(self, *args):
        if self.content_server is not None and \
                self.content_server.exception is not None:
            error_dialog(self, _('Failed to start content server'),
                         unicode(self.content_server.exception)).exec_()

    @property
    def current_db(self):
        return self.library_view.model().db

    def another_instance_wants_to_talk(self):
        try:
            msg = self.listener.queue.get_nowait()
        except Empty:
            return
        if msg.startswith('launched:'):
            import json
            try:
                argv = json.loads(msg[len('launched:'):])
            except ValueError:
                prints('Failed to decode message from other instance: %r' %
                       msg)
                if DEBUG:
                    error_dialog(
                        self,
                        'Invalid message',
                        'Received an invalid message from other calibre instance.'
                        ' Do you have multiple versions of calibre installed?',
                        det_msg='Invalid msg: %r' % msg,
                        show=True)
                argv = ()
            if isinstance(argv, (list, tuple)) and len(argv) > 1:
                files = [
                    os.path.abspath(p) for p in argv[1:]
                    if not os.path.isdir(p) and os.access(p, os.R_OK)
                ]
                if files:
                    self.iactions['Add Books'].add_filesystem_book(files)
            self.setWindowState(self.windowState() & ~Qt.WindowMinimized
                                | Qt.WindowActive)
            self.show_windows()
            self.raise_()
            self.activateWindow()
        elif msg.startswith('refreshdb:'):
            m = self.library_view.model()
            m.db.new_api.reload_from_db()
            m.db.data.refresh(clear_caches=False, do_search=False)
            m.resort()
            m.research()
            self.tags_view.recount()
        elif msg.startswith('shutdown:'):
            self.quit(confirm_quit=False)
        else:
            print msg

    def current_view(self):
        '''Convenience method that returns the currently visible view '''
        idx = self.stack.currentIndex()
        if idx == 0:
            return self.library_view
        if idx == 1:
            return self.memory_view
        if idx == 2:
            return self.card_a_view
        if idx == 3:
            return self.card_b_view

    def booklists(self):
        return self.memory_view.model().db, self.card_a_view.model(
        ).db, self.card_b_view.model().db

    def library_moved(self,
                      newloc,
                      copy_structure=False,
                      call_close=True,
                      allow_rebuild=False):
        if newloc is None:
            return
        default_prefs = None
        try:
            olddb = self.library_view.model().db
            if copy_structure:
                default_prefs = olddb.prefs

            from calibre.utils.formatter_functions import unload_user_template_functions
            unload_user_template_functions(olddb.library_id)
        except:
            olddb = None
        try:
            db = LibraryDatabase(newloc, default_prefs=default_prefs)
        except apsw.Error:
            if not allow_rebuild:
                raise
            import traceback
            repair = question_dialog(
                self,
                _('Corrupted database'),
                _('The library database at %s appears to be corrupted. Do '
                  'you want calibre to try and rebuild it automatically? '
                  'The rebuild may not be completely successful.') %
                force_unicode(newloc, filesystem_encoding),
                det_msg=traceback.format_exc())
            if repair:
                from calibre.gui2.dialogs.restore_library import repair_library_at
                if repair_library_at(newloc, parent=self):
                    db = LibraryDatabase(newloc, default_prefs=default_prefs)
                else:
                    return
            else:
                return
        if self.content_server is not None:
            self.content_server.set_database(db)
        self.library_path = newloc
        prefs['library_path'] = self.library_path
        self.book_on_device(None, reset=True)
        db.set_book_on_device_func(self.book_on_device)
        self.library_view.set_database(db)
        self.tags_view.set_database(db, self.alter_tb)
        self.library_view.model().set_book_on_device_func(self.book_on_device)
        self.status_bar.clear_message()
        self.search.clear()
        self.saved_search.clear()
        self.book_details.reset_info()
        #self.library_view.model().count_changed()
        db = self.library_view.model().db
        self.iactions['Choose Library'].count_changed(db.count())
        self.set_window_title()
        self.apply_named_search_restriction('')  # reset restriction to null
        self.saved_searches_changed(
            recount=False)  # reload the search restrictions combo box
        if db.prefs['virtual_lib_on_startup']:
            self.apply_virtual_library(db.prefs['virtual_lib_on_startup'])
        self.rebuild_vl_tabs()
        for action in self.iactions.values():
            action.library_changed(db)
        if olddb is not None:
            try:
                if call_close:
                    olddb.close()
            except:
                import traceback
                traceback.print_exc()
            olddb.break_cycles()
        if self.device_connected:
            self.set_books_in_library(self.booklists(), reset=True)
            self.refresh_ondevice()
            self.memory_view.reset()
            self.card_a_view.reset()
            self.card_b_view.reset()
        self.device_manager.set_current_library_uuid(db.library_id)
        self.library_view.set_current_row(0)
        # Run a garbage collection now so that it does not freeze the
        # interface later
        gc.collect()

    def set_window_title(self):
        db = self.current_db
        restrictions = [
            x for x in (db.data.get_base_restriction_name(),
                        db.data.get_search_restriction_name()) if x
        ]
        restrictions = ' :: '.join(restrictions)
        font = QFont()
        if restrictions:
            restrictions = ' :: ' + restrictions
            font.setBold(True)
            font.setItalic(True)
        self.virtual_library.setFont(font)
        title = u'{0} - || {1}{2} ||'.format(
            __appname__, self.iactions['Choose Library'].library_name(),
            restrictions)
        self.setWindowTitle(title)

    def location_selected(self, location):
        '''
        Called when a location icon is clicked (e.g. Library)
        '''
        page = 0 if location == 'library' else 1 if location == 'main' else 2 if location == 'carda' else 3
        self.stack.setCurrentIndex(page)
        self.book_details.reset_info()
        for x in ('tb', 'cb'):
            splitter = getattr(self, x + '_splitter')
            splitter.button.setEnabled(location == 'library')
        for action in self.iactions.values():
            action.location_selected(location)
        if location == 'library':
            self.virtual_library_menu.setEnabled(True)
            self.highlight_only_button.setEnabled(True)
        else:
            self.virtual_library_menu.setEnabled(False)
            self.highlight_only_button.setEnabled(False)
            # Reset the view in case something changed while it was invisible
            self.current_view().reset()
        self.set_number_of_books_shown()
        self.update_status_bar()

    def job_exception(self, job, dialog_title=_('Conversion Error')):
        if not hasattr(self, '_modeless_dialogs'):
            self._modeless_dialogs = []
        minz = self.is_minimized_to_tray
        if self.isVisible():
            for x in list(self._modeless_dialogs):
                if not x.isVisible():
                    self._modeless_dialogs.remove(x)
        try:
            if 'calibre.ebooks.DRMError' in job.details:
                if not minz:
                    from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                    d = DRMErrorMessage(
                        self,
                        _('Cannot convert') + ' ' +
                        job.description.split(':')[-1].partition('(')[-1][:-1])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.oeb.transforms.split.SplitError' in job.details:
                title = job.description.split(':')[-1].partition('(')[-1][:-1]
                msg = _('<p><b>Failed to convert: %s') % title
                msg += '<p>' + _('''
                Many older ebook reader devices are incapable of displaying
                EPUB files that have internal components over a certain size.
                Therefore, when converting to EPUB, calibre automatically tries
                to split up the EPUB into smaller sized pieces.  For some
                files that are large undifferentiated blocks of text, this
                splitting fails.
                <p>You can <b>work around the problem</b> by either increasing the
                maximum split size under EPUB Output in the conversion dialog,
                or by turning on Heuristic Processing, also in the conversion
                dialog. Note that if you make the maximum split size too large,
                your ebook reader may have trouble with the EPUB.
                        ''')
                if not minz:
                    d = error_dialog(self,
                                     _('Conversion Failed'),
                                     msg,
                                     det_msg=job.details)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.web.feeds.input.RecipeDisabled' in job.details:
                if not minz:
                    msg = job.details
                    msg = msg[msg.
                              find('calibre.web.feeds.input.RecipeDisabled:'):]
                    msg = msg.partition(':')[-1]
                    d = error_dialog(self, _('Recipe Disabled'),
                                     '<p>%s</p>' % msg)
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return

            if 'calibre.ebooks.conversion.ConversionUserFeedBack:' in job.details:
                if not minz:
                    import json
                    payload = job.details.rpartition(
                        'calibre.ebooks.conversion.ConversionUserFeedBack:'
                    )[-1]
                    payload = json.loads('{' + payload.partition('{')[-1])
                    d = {
                        'info': info_dialog,
                        'warn': warning_dialog,
                        'error': error_dialog
                    }.get(payload['level'], error_dialog)
                    d = d(self,
                          payload['title'],
                          '<p>%s</p>' % payload['msg'],
                          det_msg=payload['det_msg'])
                    d.setModal(False)
                    d.show()
                    self._modeless_dialogs.append(d)
                return
        except:
            pass
        if job.killed:
            return
        try:
            prints(job.details, file=sys.stderr)
        except:
            pass
        if not minz:
            self.job_error_dialog.show_error(dialog_title,
                                             _('<b>Failed</b>') + ': ' +
                                             unicode(job.description),
                                             det_msg=job.details)

    def read_settings(self):
        geometry = config['main_window_geometry']
        if geometry is not None:
            self.restoreGeometry(geometry)
        self.read_layout_settings()

    def write_settings(self):
        with gprefs:  # Only write to gprefs once
            config.set('main_window_geometry', self.saveGeometry())
            dynamic.set('sort_history', self.library_view.model().sort_history)
            self.save_layout_state()

    def quit(self,
             checked=True,
             restart=False,
             debug_on_restart=False,
             confirm_quit=True):
        if confirm_quit and not self.confirm_quit():
            return
        try:
            self.shutdown()
        except:
            pass
        self.restart_after_quit = restart
        self.debug_on_restart = debug_on_restart
        QApplication.instance().quit()

    def donate(self, *args):
        open_url(QUrl('http://calibre-ebook.com/donate'))

    def confirm_quit(self):
        if self.job_manager.has_jobs():
            msg = _('There are active jobs. Are you sure you want to quit?')
            if self.job_manager.has_device_jobs():
                msg = '<p>'+__appname__ + \
                      _(''' is communicating with the device!<br>
                      Quitting may cause corruption on the device.<br>
                      Are you sure you want to quit?''')+'</p>'

            if not question_dialog(self, _('Active jobs'), msg):
                return False
        from calibre.db.delete_service import has_jobs
        if has_jobs():
            msg = _('Some deleted books are still being moved to the Recycle '
                    'Bin, if you quit now, they will be left behind. Are you '
                    'sure you want to quit?')
            if not question_dialog(self, _('Active jobs'), msg):
                return False

        return True

    def shutdown(self, write_settings=True):
        self.grid_view.shutdown()
        try:
            db = self.library_view.model().db
            cf = db.clean
        except:
            pass
        else:
            cf()
            # Save the current field_metadata for applications like calibre2opds
            # Goes here, because if cf is valid, db is valid.
            db.prefs['field_metadata'] = db.field_metadata.all_metadata()
            db.commit_dirty_cache()
            db.prefs.write_serialized(prefs['library_path'])
        for action in self.iactions.values():
            if not action.shutting_down():
                return
        if write_settings:
            self.write_settings()
        self.check_messages_timer.stop()
        self.update_checker.terminate()
        self.listener.close()
        self.job_manager.server.close()
        self.job_manager.threaded_server.close()
        while self.spare_servers:
            self.spare_servers.pop().close()
        self.device_manager.keep_going = False
        self.auto_adder.stop()
        mb = self.library_view.model().metadata_backup
        if mb is not None:
            mb.stop()

        self.hide_windows()
        try:
            try:
                if self.content_server is not None:
                    s = self.content_server
                    self.content_server = None
                    s.exit()
            except:
                pass
        except KeyboardInterrupt:
            pass
        from calibre.db.delete_service import shutdown
        shutdown()
        time.sleep(2)
        self.istores.join()
        self.hide_windows()
        # Do not report any errors that happen after the shutdown
        sys.excepthook = sys.__excepthook__
        return True

    def run_wizard(self, *args):
        if self.confirm_quit():
            self.run_wizard_b4_shutdown = True
            self.restart_after_quit = True
            try:
                self.shutdown(write_settings=False)
            except:
                pass
            QApplication.instance().quit()

    def closeEvent(self, e):
        self.write_settings()
        if self.system_tray_icon.isVisible():
            if not dynamic['systray_msg'] and not isosx:
                info_dialog(
                    self,
                    'calibre',
                    'calibre ' +
                    _('will keep running in the system tray. To close it, '
                      'choose <b>Quit</b> in the context menu of the '
                      'system tray.'),
                    show_copy_button=False).exec_()
                dynamic['systray_msg'] = True
            self.hide_windows()
            e.ignore()
        else:
            if self.confirm_quit():
                try:
                    self.shutdown(write_settings=False)
                except:
                    import traceback
                    traceback.print_exc()
                e.accept()
            else:
                e.ignore()
Exemple #43
0
class Scheduler(QObject):

    INTERVAL = 1  # minutes

    delete_old_news = pyqtSignal(object)
    start_recipe_fetch = pyqtSignal(object)

    def __init__(self, parent, db):
        QObject.__init__(self, parent)
        self.internet_connection_failed = False
        self._parent = parent
        self.no_internet_msg = _('Cannot download news as no internet connection '
                'is active')
        self.no_internet_dialog = d = error_dialog(self._parent,
                self.no_internet_msg, _('No internet connection'),
                show_copy_button=False)
        d.setModal(False)

        self.recipe_model = RecipeModel()
        self.db = db
        self.lock = QMutex(QMutex.Recursive)
        self.download_queue = set([])

        self.news_menu = QMenu()
        self.news_icon = QIcon(I('news.png'))
        self.scheduler_action = QAction(QIcon(I('scheduler.png')), _('Schedule news download'), self)
        self.news_menu.addAction(self.scheduler_action)
        self.scheduler_action.triggered[bool].connect(self.show_dialog)
        self.cac = QAction(QIcon(I('user_profile.png')), _('Add a custom news source'), self)
        self.cac.triggered[bool].connect(self.customize_feeds)
        self.news_menu.addAction(self.cac)
        self.news_menu.addSeparator()
        self.all_action = self.news_menu.addAction(
                _('Download all scheduled news sources'),
                self.download_all_scheduled)

        self.timer = QTimer(self)
        self.timer.start(int(self.INTERVAL * 60 * 1000))
        self.timer.timeout.connect(self.check)
        self.oldest = gconf['oldest_news']
        QTimer.singleShot(5 * 1000, self.oldest_check)

    def database_changed(self, db):
        self.db = db

    def oldest_check(self):
        if self.oldest > 0:
            delta = timedelta(days=self.oldest)
            try:
                ids = list(self.db.tags_older_than(_('News'),
                    delta, must_have_authors=['calibre']))
            except:
                # Happens if library is being switched
                ids = []
            if ids:
                if ids:
                    self.delete_old_news.emit(ids)
        QTimer.singleShot(60 * 60 * 1000, self.oldest_check)

    def show_dialog(self, *args):
        self.lock.lock()
        try:
            d = SchedulerDialog(self.recipe_model)
            d.download.connect(self.download_clicked)
            d.exec_()
            gconf['oldest_news'] = self.oldest = d.old_news.value()
            d.break_cycles()
        finally:
            self.lock.unlock()

    def customize_feeds(self, *args):
        from calibre.gui2.dialogs.user_profiles import UserProfiles
        d = UserProfiles(self._parent, self.recipe_model)
        d.exec_()
        d.break_cycles()

    def do_download(self, urn):
        self.lock.lock()
        try:
            account_info = self.recipe_model.get_account_info(urn)
            customize_info = self.recipe_model.get_customize_info(urn)
            recipe = self.recipe_model.recipe_from_urn(urn)
            un = pw = None
            if account_info is not None:
                un, pw = account_info
            add_title_tag, custom_tags, keep_issues = customize_info
            script = self.recipe_model.get_recipe(urn)
            pt = PersistentTemporaryFile('_builtin.recipe')
            pt.write(script)
            pt.close()
            arg = {
                    'username': un,
                    'password': pw,
                    'add_title_tag':add_title_tag,
                    'custom_tags':custom_tags,
                    'recipe':pt.name,
                    'title':recipe.get('title',''),
                    'urn':urn,
                    'keep_issues':keep_issues
                   }
            self.download_queue.add(urn)
            self.start_recipe_fetch.emit(arg)
        finally:
            self.lock.unlock()

    def recipe_downloaded(self, arg):
        self.lock.lock()
        try:
            self.recipe_model.update_last_downloaded(arg['urn'])
            self.download_queue.remove(arg['urn'])
        finally:
            self.lock.unlock()

    def recipe_download_failed(self, arg):
        self.lock.lock()
        try:
            self.recipe_model.update_last_downloaded(arg['urn'])
            self.download_queue.remove(arg['urn'])
        finally:
            self.lock.unlock()

    def download_clicked(self, urn):
        if urn is not None:
            return self.download(urn)
        for urn in self.recipe_model.scheduled_urns():
            if not self.download(urn):
                break

    def download_all_scheduled(self):
        self.download_clicked(None)

    def has_internet_connection(self):
        if not internet_connected():
            if not self.internet_connection_failed:
                self.internet_connection_failed = True
                if self._parent.is_minimized_to_tray:
                    self._parent.status_bar.show_message(self.no_internet_msg,
                            5000)
                elif not self.no_internet_dialog.isVisible():
                    self.no_internet_dialog.show()
            return False
        self.internet_connection_failed = False
        if self.no_internet_dialog.isVisible():
            self.no_internet_dialog.hide()
        return True

    def download(self, urn):
        self.lock.lock()
        if not self.has_internet_connection():
            return False
        doit = urn not in self.download_queue
        self.lock.unlock()
        if doit:
            self.do_download(urn)
        return True

    def check(self):
        recipes = self.recipe_model.get_to_be_downloaded_recipes()
        for urn in recipes:
            if not self.download(urn):
                # No internet connection, we will try again in a minute
                break
Exemple #44
0
class GEditorMenuBar(QMenuBar):
    def __init__(self, parent):
        QMenuBar.__init__(self, parent)

        # Menu items
        self._fileM = QMenu(self.tr("File"))
        self._editM = QMenu(self.tr("Edit"))
        self._viewM = QMenu(self.tr("View"))
        self._optionM = QMenu(self.tr("Options"))

        self.addMenu(self._fileM)
        self.addMenu(self._editM)
        self.addMenu(self._viewM)
        self.addMenu(self._optionM)

        # Actions
        self._connectDatabaseA = QAction(self.tr("Connect a PostGIS database"),
                                         self.parent())
        self._connectDatabaseA.setShortcut(QKeySequence("Ctrl+Shift+C"))
        self.parent().connect(self._connectDatabaseA, SIGNAL("triggered()"),
                              self.parent().dialConnectDatabase)

        self._importDatabaseDataA = QAction(
            self.tr("Import data from database"), self.parent())
        self._importDatabaseDataA.setShortcut(QKeySequence("Ctrl+Shift+I"))
        self.parent().connect(self._importDatabaseDataA, SIGNAL("triggered()"),
                              self.parent().dialImportDatabaseData)

        self._importVectorFileDataA = QAction(
            self.tr("Import vector from file"), self.parent())
        self._importVectorFileDataA.setShortcut(QKeySequence("Ctrl+Shift+V"))
        self.parent().connect(self._importVectorFileDataA,
                              SIGNAL("triggered()"),
                              self.parent().dialImportVectorFileData)

        self._importRasterFileDataA = QAction(
            self.tr("Import raster data from file"), self.parent())
        self._importRasterFileDataA.setShortcut(QKeySequence("Ctrl+Shift+R"))
        self.parent().connect(self._importRasterFileDataA,
                              SIGNAL("triggered()"),
                              self.parent().dialImportRasterFileData)

        self._colorLayerA = QAction(self.tr("Select layer color"),
                                    self.parent())
        self.parent().connect(self._colorLayerA, SIGNAL("triggered()"),
                              self.parent().dialSelectLayerColor)

        # Add actions to menu
        self._fileM.addAction(self._importVectorFileDataA)
        self._fileM.addAction(self._importRasterFileDataA)
        self._fileM.addSeparator()
        self._fileM.addAction(self._connectDatabaseA)
        self._fileM.addAction(self._importDatabaseDataA)

        self._viewM.addAction(self._colorLayerA)

    def dial(self):
        dial = QDialog(self)
        dial.setWindowTitle("Select a layer")
        layerList = QListWidget(dial)
        db = GPostGISDatabase()
        for t in db.tables():
            layerList.addItem(t)
        dial.setBaseSize(layerList.minimumSize())
        dial.show()
Exemple #45
0
 def show_context_menu(self, point):
     idx = self.currentIndex()
     if idx and idx.isValid() and not idx.data(Qt.UserRole).toPyObject():
         m = QMenu()
         m.addAction(QIcon(I('view.png')), _('View this cover at full size'), self.show_cover)
         m.exec_(QCursor.pos())
Exemple #46
0
    def __init__(self, image, parent, imgman, name=None, save=False):
        QFrame.__init__(self, parent)
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        # init state
        self.image = image
        self._imgman = imgman
        self._currier = PersistentCurrier()
        self._control_dialog = None
        # create widgets
        self._lo = lo = QHBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo.setSpacing(2)
        # raise button
        self._wraise = QToolButton(self)
        lo.addWidget(self._wraise)
        self._wraise.setIcon(pixmaps.raise_up.icon())
        self._wraise.setAutoRaise(True)
        self._can_raise = False
        QObject.connect(self._wraise, SIGNAL("clicked()"), self._raiseButtonPressed)
        self._wraise.setToolTip("""<P>Click here to raise this image above other images. Hold the button down briefly to
      show a menu of image operations.</P>""")
        # center label
        self._wcenter = QLabel(self)
        self._wcenter.setPixmap(pixmaps.center_image.pm())
        self._wcenter.setToolTip(
            "<P>The plot is currently centered on (the reference pixel %d,%d) of this image.</P>" % self.image.referencePixel())
        lo.addWidget(self._wcenter)
        # name/filename label
        self.name = image.name
        self._wlabel = QLabel(self.name, self)
        self._number = 0
        self.setName(self.name)
        self._wlabel.setToolTip("%s %s" % (image.filename, "\u00D7".join(map(str, image.data().shape))))
        lo.addWidget(self._wlabel, 1)
        # if 'save' is specified, create a "save" button
        if save:
            self._wsave = QToolButton(self)
            lo.addWidget(self._wsave)
            self._wsave.setText("save")
            self._wsave.setAutoRaise(True)
            self._save_dir = save if isinstance(save, str) else "."
            QObject.connect(self._wsave, SIGNAL("clicked()"), self._saveImage)
            self._wsave.setToolTip("""<P>Click here to write this image to a FITS file.</P>""")
        # render control
        dprint(2, "creating RenderControl")
        self._rc = RenderControl(image, self)
        dprint(2, "done")
        # selectors for extra axes
        self._wslicers = []
        curslice = self._rc.currentSlice();  # this may be loaded from config, so not necessarily 0
        for iextra, axisname, labels in self._rc.slicedAxes():
            if axisname.upper() not in ["STOKES", "COMPLEX"]:
                lbl = QLabel("%s:" % axisname, self)
                lo.addWidget(lbl)
            else:
                lbl = None
            slicer = QComboBox(self)
            self._wslicers.append(slicer)
            lo.addWidget(slicer)
            slicer.addItems(labels)
            slicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % axisname)
            slicer.setCurrentIndex(curslice[iextra])
            QObject.connect(slicer, SIGNAL("activated(int)"), self._currier.curry(self._rc.changeSlice, iextra))
        # min/max display ranges
        lo.addSpacing(5)
        self._wrangelbl = QLabel(self)
        lo.addWidget(self._wrangelbl)
        self._minmaxvalidator = FloatValidator(self)
        self._wmin = QLineEdit(self)
        self._wmax = QLineEdit(self)
        width = self._wmin.fontMetrics().width("1.234567e-05")
        for w in self._wmin, self._wmax:
            lo.addWidget(w, 0)
            w.setValidator(self._minmaxvalidator)
            w.setMaximumWidth(width)
            w.setMinimumWidth(width)
            QObject.connect(w, SIGNAL("editingFinished()"), self._changeDisplayRange)
        # full-range button
        self._wfullrange = QToolButton(self)
        lo.addWidget(self._wfullrange, 0)
        self._wfullrange.setIcon(pixmaps.zoom_range.icon())
        self._wfullrange.setAutoRaise(True)
        QObject.connect(self._wfullrange, SIGNAL("clicked()"), self.renderControl().resetSubsetDisplayRange)
        rangemenu = QMenu(self)
        rangemenu.addAction(pixmaps.full_range.icon(), "Full subset", self.renderControl().resetSubsetDisplayRange)
        for percent in (99.99, 99.9, 99.5, 99, 98, 95):
            rangemenu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent))
        self._wfullrange.setPopupMode(QToolButton.DelayedPopup)
        self._wfullrange.setMenu(rangemenu)
        # update widgets from current display range
        self._updateDisplayRange(*self._rc.displayRange())
        # lock button
        self._wlock = QToolButton(self)
        self._wlock.setIcon(pixmaps.unlocked.icon())
        self._wlock.setAutoRaise(True)
        self._wlock.setToolTip("""<P>Click to lock or unlock the intensity range. When the intensity range is locked across multiple images, any changes in the intensity
          range of one are propagated to the others. Hold the button down briefly for additional options.</P>""")
        lo.addWidget(self._wlock)
        QObject.connect(self._wlock, SIGNAL("clicked()"), self._toggleDisplayRangeLock)
        QObject.connect(self.renderControl(), SIGNAL("displayRangeLocked"), self._setDisplayRangeLock)
        QObject.connect(self.renderControl(), SIGNAL("dataSubsetChanged"), self._dataSubsetChanged)
        lockmenu = QMenu(self)
        lockmenu.addAction(pixmaps.locked.icon(), "Lock all to this",
                           self._currier.curry(imgman.lockAllDisplayRanges, self.renderControl()))
        lockmenu.addAction(pixmaps.unlocked.icon(), "Unlock all", imgman.unlockAllDisplayRanges)
        self._wlock.setPopupMode(QToolButton.DelayedPopup)
        self._wlock.setMenu(lockmenu)
        self._setDisplayRangeLock(self.renderControl().isDisplayRangeLocked())
        # dialog button
        self._wshowdialog = QToolButton(self)
        lo.addWidget(self._wshowdialog)
        self._wshowdialog.setIcon(pixmaps.colours.icon())
        self._wshowdialog.setAutoRaise(True)
        self._wshowdialog.setToolTip("""<P>Click for colourmap and intensity policy options.</P>""")
        QObject.connect(self._wshowdialog, SIGNAL("clicked()"), self.showRenderControls)
        tooltip = """<P>You can change the currently displayed intensity range by entering low and high limits here.</P>
    <TABLE>
      <TR><TD><NOBR>Image min:</NOBR></TD><TD>%g</TD><TD>max:</TD><TD>%g</TD></TR>
      </TABLE>""" % self.image.imageMinMax()
        for w in self._wmin, self._wmax, self._wrangelbl:
            w.setToolTip(tooltip)

        # create image operations menu
        self._menu = QMenu(self.name, self)
        self._qa_raise = self._menu.addAction(pixmaps.raise_up.icon(), "Raise image",
                                              self._currier.curry(self.image.emit, SIGNAL("raise")))
        self._qa_center = self._menu.addAction(pixmaps.center_image.icon(), "Center plot on image",
                                               self._currier.curry(self.image.emit, SIGNAL("center")))
        self._qa_show_rc = self._menu.addAction(pixmaps.colours.icon(), "Colours && Intensities...",
                                                self.showRenderControls)
        if save:
            self._qa_save = self._menu.addAction("Save image...", self._saveImage)
        self._menu.addAction("Export image to PNG file...", self._exportImageToPNG)
        self._export_png_dialog = None
        self._menu.addAction("Unload image", self._currier.curry(self.image.emit, SIGNAL("unload")))
        self._wraise.setMenu(self._menu)
        self._wraise.setPopupMode(QToolButton.DelayedPopup)

        # connect updates from renderControl and image
        self.image.connect(SIGNAL("slice"), self._updateImageSlice)
        QObject.connect(self._rc, SIGNAL("displayRangeChanged"), self._updateDisplayRange)

        # default plot depth of image markers
        self._z_markers = None
        # and the markers themselves
        self._image_border = QwtPlotCurve()
        self._image_label = QwtPlotMarker()

        # subset markers
        self._subset_pen = QPen(QColor("Light Blue"))
        self._subset_border = QwtPlotCurve()
        self._subset_border.setPen(self._subset_pen)
        self._subset_border.setVisible(False)
        self._subset_label = QwtPlotMarker()
        text = QwtText("subset")
        text.setColor(self._subset_pen.color())
        self._subset_label.setLabel(text)
        self._subset_label.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom)
        self._subset_label.setVisible(False)
        self._setting_lmrect = False

        self._all_markers = [self._image_border, self._image_label, self._subset_border, self._subset_label]
Exemple #47
0
class ShareConnMenu(QMenu):  # {{{

    connect_to_folder = pyqtSignal()
    connect_to_itunes = pyqtSignal()
    connect_to_bambook = pyqtSignal()

    config_email = pyqtSignal()
    toggle_server = pyqtSignal()
    control_smartdevice = pyqtSignal()
    dont_add_to = frozenset(['context-menu-device'])

    DEVICE_MSGS = [_('Start wireless device connection'),
            _('Stop wireless device connection')]

    def __init__(self, parent=None):
        QMenu.__init__(self, parent)
        mitem = self.addAction(QIcon(I('devices/folder.png')), _('Connect to folder'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
        self.connect_to_folder_action = mitem
        mitem = self.addAction(QIcon(I('devices/itunes.png')),
                _('Connect to iTunes'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
        self.connect_to_itunes_action = mitem
        itunes_ok = iswindows or (isosx and get_osx_version() < (10, 9, 0))
        mitem.setVisible(itunes_ok)
        mitem = self.addAction(QIcon(I('devices/bambook.png')), _('Connect to Bambook'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x : self.connect_to_bambook.emit())
        self.connect_to_bambook_action = mitem
        bambook_visible = False
        if not is_disabled(BAMBOOK):
            device_ip = BAMBOOK.settings().extra_customization
            if device_ip:
                bambook_visible = True
        self.connect_to_bambook_action.setVisible(bambook_visible)

        self.addSeparator()
        self.toggle_server_action = \
            self.addAction(QIcon(I('network-server.png')),
            _('Start Content Server'))
        self.toggle_server_action.triggered.connect(lambda x:
                self.toggle_server.emit())
        self.control_smartdevice_action = \
            self.addAction(QIcon(I('dot_red.png')),
            self.DEVICE_MSGS[0])
        self.control_smartdevice_action.triggered.connect(lambda x:
                self.control_smartdevice.emit())
        self.addSeparator()

        self.email_actions = []

        if hasattr(parent, 'keyboard'):
            r = parent.keyboard.register_shortcut
            prefix = 'Share/Connect Menu '
            gr = ConnectShareAction.action_spec[0]
            for attr in ('folder', 'bambook', 'itunes'):
                if not (iswindows or isosx) and attr == 'itunes':
                    continue
                ac = getattr(self, 'connect_to_%s_action'%attr)
                r(prefix + attr, unicode(ac.text()), action=ac,
                        group=gr)
            r(prefix+' content server', _('Start/stop content server'),
                    action=self.toggle_server_action, group=gr)

    def server_state_changed(self, running):
        from calibre.utils.mdns import get_external_ip, verify_ipV4_address
        text = _('Start Content Server')
        if running:
            listen_on = (verify_ipV4_address(tweaks['server_listen_on']) or
                    get_external_ip())
            try :
                cs_port = content_server_config().parse().port
                ip_text = _(' [%(ip)s, port %(port)d]')%dict(ip=listen_on,
                        port=cs_port)
            except:
                ip_text = ' [%s]'%listen_on
            text = _('Stop Content Server') + ip_text
        self.toggle_server_action.setText(text)

    def hide_smartdevice_menus(self):
        self.control_smartdevice_action.setVisible(False)

    def build_email_entries(self, sync_menu):
        from calibre.gui2.device import DeviceAction
        for ac in self.email_actions:
            self.removeAction(ac)
        self.email_actions = []
        self.memory = []
        opts = email_config().parse()
        if opts.accounts:
            self.email_to_menu = QMenu(_('Email to')+'...', self)
            ac = self.addMenu(self.email_to_menu)
            self.email_actions.append(ac)
            self.email_to_and_delete_menu = QMenu(
                    _('Email to and delete from library')+'...', self)
            keys = sorted(opts.accounts.keys())
            for account in keys:
                formats, auto, default = opts.accounts[account]
                subject = opts.subjects.get(account, '')
                alias = opts.aliases.get(account, '')
                dest = 'mail:'+account+';'+formats+';'+subject
                action1 = DeviceAction(dest, False, False, I('mail.png'),
                        alias or account)
                action2 = DeviceAction(dest, True, False, I('mail.png'),
                        (alias or account) + ' ' + _('(delete from library)'))
                self.email_to_menu.addAction(action1)
                self.email_to_and_delete_menu.addAction(action2)
                map(self.memory.append, (action1, action2))
                if default:
                    ac = DeviceAction(dest, False, False,
                            I('mail.png'), _('Email to') + ' ' +(alias or
                                account))
                    self.addAction(ac)
                    self.email_actions.append(ac)
                    ac.a_s.connect(sync_menu.action_triggered)
                action1.a_s.connect(sync_menu.action_triggered)
                action2.a_s.connect(sync_menu.action_triggered)
            action1 = DeviceAction('choosemail:', False, False, I('mail.png'),
                    _('Select recipients'))
            action2 = DeviceAction('choosemail:', True, False, I('mail.png'),
                    _('Select recipients') + ' ' + _('(delete from library)'))
            self.email_to_menu.addAction(action1)
            self.email_to_and_delete_menu.addAction(action2)
            map(self.memory.append, (action1, action2))
            tac1 = DeviceAction('choosemail:', False, False, I('mail.png'),
                    _('Email to selected recipients...'))
            self.addAction(tac1)
            tac1.a_s.connect(sync_menu.action_triggered)
            self.memory.append(tac1)
            ac = self.addMenu(self.email_to_and_delete_menu)
            self.email_actions.append(ac)
            action1.a_s.connect(sync_menu.action_triggered)
            action2.a_s.connect(sync_menu.action_triggered)
        else:
            ac = self.addAction(_('Setup email based sharing of books'))
            self.email_actions.append(ac)
            ac.triggered.connect(self.setup_email)

    def setup_email(self, *args):
        self.config_email.emit()

    def set_state(self, device_connected, device):
        self.connect_to_folder_action.setEnabled(not device_connected)
        self.connect_to_itunes_action.setEnabled(not device_connected)
        self.connect_to_bambook_action.setEnabled(not device_connected)
Exemple #48
0
    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)
class LocationManager(QObject):  # {{{

    locations_changed = pyqtSignal()
    unmount_device = pyqtSignal()
    location_selected = pyqtSignal(object)
    configure_device = 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)

            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
Exemple #50
0
class EbookViewer(MainWindow, Ui_EbookViewer):

    STATE_VERSION = 1
    FLOW_MODE_TT = _('Switch to paged mode - where the text is broken up '
                     'into pages like a paper book')
    PAGED_MODE_TT = _('Switch to flow mode - where the text is not broken up '
                      'into pages')

    def __init__(self,
                 pathtoebook=None,
                 debug_javascript=False,
                 open_at=None,
                 start_in_fullscreen=False):
        MainWindow.__init__(self, None)
        self.setupUi(self)
        self.view.initialize_view(debug_javascript)
        self.view.magnification_changed.connect(self.magnification_changed)
        self.show_toc_on_open = False
        self.current_book_has_toc = False
        self.base_window_title = unicode(self.windowTitle())
        self.iterator = None
        self.current_page = None
        self.pending_search = None
        self.pending_search_dir = None
        self.pending_anchor = None
        self.pending_reference = None
        self.pending_bookmark = None
        self.pending_restore = False
        self.existing_bookmarks = []
        self.selected_text = None
        self.was_maximized = False
        self.read_settings()
        self.dictionary_box.hide()
        self.close_dictionary_view.clicked.connect(
            lambda x: self.dictionary_box.hide())
        self.history = History(self.action_back, self.action_forward)
        self.metadata = Metadata(self)
        self.pos = DoubleSpinBox()
        self.pos.setDecimals(1)
        self.pos.setSuffix('/' + _('Unknown') + '     ')
        self.pos.setMinimum(1.)
        self.pos.value_changed.connect(self.update_pos_label)
        self.splitter.setCollapsible(0, False)
        self.splitter.setCollapsible(1, False)
        self.pos.setMinimumWidth(150)
        self.tool_bar2.insertWidget(self.action_find_next, self.pos)
        self.reference = Reference()
        self.tool_bar2.insertSeparator(self.action_find_next)
        self.tool_bar2.insertWidget(self.action_find_next, self.reference)
        self.tool_bar2.insertSeparator(self.action_find_next)
        self.setFocusPolicy(Qt.StrongFocus)
        self.search = SearchBox2(self)
        self.search.setMinimumContentsLength(20)
        self.search.initialize('viewer_search_history')
        self.search.setToolTip(_('Search for text in book'))
        self.search.setMinimumWidth(200)
        self.tool_bar2.insertWidget(self.action_find_next, self.search)
        self.view.set_manager(self)
        self.pi = ProgressIndicator(self)
        self.toc.setVisible(False)
        self.action_quit = QAction(_('&Quit'), self)
        self.addAction(self.action_quit)
        self.view_resized_timer = QTimer(self)
        self.view_resized_timer.timeout.connect(self.viewport_resize_finished)
        self.view_resized_timer.setSingleShot(True)
        self.resize_in_progress = False
        qs = [Qt.CTRL + Qt.Key_Q, Qt.CTRL + Qt.Key_W]
        self.action_quit.setShortcuts(qs)
        self.action_quit.triggered.connect(self.quit)
        self.action_focus_search = QAction(self)
        self.addAction(self.action_focus_search)
        self.action_focus_search.setShortcuts(
            [Qt.Key_Slash, QKeySequence(QKeySequence.Find)])
        self.action_focus_search.triggered.connect(
            lambda x: self.search.setFocus(Qt.OtherFocusReason))
        self.action_copy.setDisabled(True)
        self.action_metadata.setCheckable(True)
        self.action_metadata.setShortcut(Qt.CTRL + Qt.Key_I)
        self.action_table_of_contents.setCheckable(True)
        self.toc.setMinimumWidth(80)
        self.action_reference_mode.setCheckable(True)
        self.action_reference_mode.triggered[bool].connect(
            self.view.reference_mode)
        self.action_metadata.triggered[bool].connect(self.metadata.setVisible)
        self.action_table_of_contents.toggled[bool].connect(
            self.set_toc_visible)
        self.action_copy.triggered[bool].connect(self.copy)
        self.action_font_size_larger.triggered.connect(self.font_size_larger)
        self.action_font_size_smaller.triggered.connect(self.font_size_smaller)
        self.action_font_size_larger.setShortcut(Qt.CTRL + Qt.Key_Equal)
        self.action_font_size_smaller.setShortcut(Qt.CTRL + Qt.Key_Minus)
        self.action_open_ebook.triggered[bool].connect(self.open_ebook)
        self.action_next_page.triggered.connect(self.view.next_page)
        self.action_previous_page.triggered.connect(self.view.previous_page)
        self.action_find_next.triggered.connect(self.find_next)
        self.action_find_previous.triggered.connect(self.find_previous)
        self.action_full_screen.triggered[bool].connect(self.toggle_fullscreen)
        self.action_full_screen.setShortcuts(
            [Qt.Key_F11, Qt.CTRL + Qt.SHIFT + Qt.Key_F])
        self.action_full_screen.setToolTip(
            _('Toggle full screen (%s)') % _(' or ').join([
                unicode(x.toString(x.NativeText))
                for x in self.action_full_screen.shortcuts()
            ]))
        self.action_back.triggered[bool].connect(self.back)
        self.action_forward.triggered[bool].connect(self.forward)
        self.action_bookmark.triggered[bool].connect(self.bookmark)
        self.action_preferences.triggered.connect(self.do_config)
        self.pos.editingFinished.connect(self.goto_page_num)
        self.vertical_scrollbar.valueChanged[int].connect(
            lambda x: self.goto_page(x / 100.))
        self.search.search.connect(self.find)
        self.search.focus_to_library.connect(
            lambda: self.view.setFocus(Qt.OtherFocusReason))
        self.toc.pressed[QModelIndex].connect(self.toc_clicked)
        self.reference.goto.connect(self.goto)

        self.bookmarks_menu = QMenu()
        self.action_bookmark.setMenu(self.bookmarks_menu)
        self.set_bookmarks([])

        self.themes_menu = QMenu()
        self.action_load_theme.setMenu(self.themes_menu)
        self.tool_bar.widgetForAction(self.action_load_theme).setPopupMode(
            QToolButton.InstantPopup)
        self.load_theme_menu()

        if pathtoebook is not None:
            f = functools.partial(self.load_ebook,
                                  pathtoebook,
                                  open_at=open_at)
            QTimer.singleShot(50, f)
        self.view.setMinimumSize(100, 100)
        self.toc.setCursor(Qt.PointingHandCursor)
        self.tool_bar.setContextMenuPolicy(Qt.PreventContextMenu)
        self.tool_bar2.setContextMenuPolicy(Qt.PreventContextMenu)
        self.tool_bar.widgetForAction(self.action_bookmark).setPopupMode(
            QToolButton.MenuButtonPopup)
        self.action_full_screen.setCheckable(True)
        self.full_screen_label = QLabel(
            '''
                <center>
                <h1>%s</h1>
                <h3>%s</h3>
                <h3>%s</h3>
                <h3>%s</h3>
                </center>
                ''' %
            (_('Full screen mode'), _('Right click to show controls'),
             _('Tap in the left or right page margin to turn pages'),
             _('Press Esc to quit')), self)
        self.full_screen_label.setVisible(False)
        self.full_screen_label.setStyleSheet('''
        QLabel {
            text-align: center;
            background-color: white;
            color: black;
            border-width: 1px;
            border-style: solid;
            border-radius: 20px;
        }
        ''')
        self.window_mode_changed = None
        self.toggle_toolbar_action = QAction(_('Show/hide controls'), self)
        self.toggle_toolbar_action.setCheckable(True)
        self.toggle_toolbar_action.triggered.connect(self.toggle_toolbars)
        self.toolbar_hidden = None
        self.addAction(self.toggle_toolbar_action)
        self.full_screen_label_anim = QPropertyAnimation(
            self.full_screen_label, 'size')
        self.clock_label = QLabel('99:99', self)
        self.clock_label.setVisible(False)
        self.clock_label.setFocusPolicy(Qt.NoFocus)
        self.info_label_style = '''
            QLabel {
                text-align: center;
                border-width: 1px;
                border-style: solid;
                border-radius: 8px;
                background-color: %s;
                color: %s;
                font-family: monospace;
                font-size: larger;
                padding: 5px;
        }'''
        self.original_frame_style = self.frame.frameStyle()
        self.pos_label = QLabel('2000/4000', self)
        self.pos_label.setVisible(False)
        self.pos_label.setFocusPolicy(Qt.NoFocus)
        self.clock_timer = QTimer(self)
        self.clock_timer.timeout.connect(self.update_clock)
        self.esc_full_screen_action = a = QAction(self)
        self.addAction(a)
        a.setShortcut(Qt.Key_Escape)
        a.setEnabled(False)
        a.triggered.connect(self.action_full_screen.trigger)

        self.print_menu = QMenu()
        self.print_menu.addAction(QIcon(I('print-preview.png')),
                                  _('Print Preview'))
        self.action_print.setMenu(self.print_menu)
        self.tool_bar.widgetForAction(self.action_print).setPopupMode(
            QToolButton.MenuButtonPopup)
        self.action_print.triggered.connect(self.print_book)
        self.print_menu.actions()[0].triggered.connect(self.print_preview)
        ca = self.view.copy_action
        ca.setShortcut(QKeySequence.Copy)
        self.addAction(ca)
        self.open_history_menu = QMenu()
        self.clear_recent_history_action = QAction(
            _('Clear list of recently opened books'), self)
        self.clear_recent_history_action.triggered.connect(
            self.clear_recent_history)
        self.build_recent_menu()
        self.action_open_ebook.setMenu(self.open_history_menu)
        self.open_history_menu.triggered[QAction].connect(self.open_recent)
        w = self.tool_bar.widgetForAction(self.action_open_ebook)
        w.setPopupMode(QToolButton.MenuButtonPopup)

        for x in ('tool_bar', 'tool_bar2'):
            x = getattr(self, x)
            for action in x.actions():
                # So that the keyboard shortcuts for these actions will
                # continue to function even when the toolbars are hidden
                self.addAction(action)

        self.view.document.settings_changed.connect(self.settings_changed)

        self.restore_state()
        self.settings_changed()
        self.action_toggle_paged_mode.toggled[bool].connect(
            self.toggle_paged_mode)
        if (start_in_fullscreen or self.view.document.start_in_fullscreen):
            self.action_full_screen.trigger()

    def toggle_paged_mode(self, checked, at_start=False):
        in_paged_mode = not self.action_toggle_paged_mode.isChecked()
        self.view.document.in_paged_mode = in_paged_mode
        self.action_toggle_paged_mode.setToolTip(
            self.FLOW_MODE_TT if self.action_toggle_paged_mode.isChecked(
            ) else self.PAGED_MODE_TT)
        if at_start: return
        self.reload()

    def settings_changed(self):
        for x in ('', '2'):
            x = getattr(self, 'tool_bar' + x)
            x.setVisible(self.view.document.show_controls)

    def reload(self):
        if hasattr(self, 'current_index') and self.current_index > -1:
            self.view.document.page_position.save(overwrite=False)
            self.pending_restore = True
            self.load_path(self.view.last_loaded_path)

    def set_toc_visible(self, yes):
        self.toc.setVisible(yes)

    def clear_recent_history(self, *args):
        vprefs.set('viewer_open_history', [])
        self.build_recent_menu()

    def build_recent_menu(self):
        m = self.open_history_menu
        m.clear()
        recent = vprefs.get('viewer_open_history', [])
        if recent:
            m.addAction(self.clear_recent_history_action)
            m.addSeparator()
        count = 0
        for path in recent:
            if count > 9:
                break
            if os.path.exists(path):
                m.addAction(RecentAction(path, m))
                count += 1

    def shutdown(self):
        if self.isFullScreen() and not self.view.document.start_in_fullscreen:
            self.action_full_screen.trigger()
            return False
        self.save_state()
        return True

    def quit(self):
        if self.shutdown():
            QApplication.instance().quit()

    def closeEvent(self, e):
        if self.shutdown():
            return MainWindow.closeEvent(self, e)
        else:
            e.ignore()

    def toggle_toolbars(self):
        for x in ('tool_bar', 'tool_bar2'):
            x = getattr(self, x)
            x.setVisible(not x.isVisible())

    def save_state(self):
        state = bytearray(self.saveState(self.STATE_VERSION))
        vprefs['viewer_toolbar_state'] = state
        if not self.isFullScreen():
            vprefs.set('viewer_window_geometry',
                       bytearray(self.saveGeometry()))
        if self.current_book_has_toc:
            vprefs.set('viewer_toc_isvisible', bool(self.toc.isVisible()))
        if self.toc.isVisible():
            vprefs.set('viewer_splitter_state',
                       bytearray(self.splitter.saveState()))
        vprefs['multiplier'] = self.view.multiplier
        vprefs['in_paged_mode'] = not self.action_toggle_paged_mode.isChecked()

    def restore_state(self):
        state = vprefs.get('viewer_toolbar_state', None)
        if state is not None:
            try:
                state = QByteArray(state)
                self.restoreState(state, self.STATE_VERSION)
            except:
                pass
        mult = vprefs.get('multiplier', None)
        if mult:
            self.view.multiplier = mult
        # On windows Qt lets the user hide toolbars via a right click in a very
        # specific location, ensure they are visible.
        self.tool_bar.setVisible(True)
        self.tool_bar2.setVisible(True)
        self.action_toggle_paged_mode.setChecked(
            not vprefs.get('in_paged_mode', True))
        self.toggle_paged_mode(self.action_toggle_paged_mode.isChecked(),
                               at_start=True)

    def lookup(self, word):
        self.dictionary_view.setHtml('<html><body><p>'+ \
            _('Connecting to dict.org to lookup: <b>%s</b>&hellip;')%word + \
            '</p></body></html>')
        self.dictionary_box.show()
        self._lookup = Lookup(word, parent=self)
        self._lookup.finished.connect(self.looked_up)
        self._lookup.start()

    def looked_up(self, *args):
        html = self._lookup.html_result
        self._lookup = None
        self.dictionary_view.setHtml(html)

    def get_remember_current_page_opt(self):
        from calibre.gui2.viewer.documentview import config
        c = config().parse()
        return c.remember_current_page

    def print_book(self):
        p = Printing(self.iterator, self)
        p.start_print()

    def print_preview(self):
        p = Printing(self.iterator, self)
        p.start_preview()

    def toggle_fullscreen(self, x):
        if self.isFullScreen():
            self.showNormal()
        else:
            self.showFullScreen()

    def showFullScreen(self):
        self.view.document.page_position.save()
        self.window_mode_changed = 'fullscreen'
        self.tool_bar.setVisible(False)
        self.tool_bar2.setVisible(False)
        self.was_maximized = self.isMaximized()
        if not self.view.document.fullscreen_scrollbar:
            self.vertical_scrollbar.setVisible(False)
            self.frame.layout().setSpacing(0)
        self._original_frame_margins = (
            self.centralwidget.layout().contentsMargins(),
            self.frame.layout().contentsMargins())
        self.frame.layout().setContentsMargins(0, 0, 0, 0)
        self.centralwidget.layout().setContentsMargins(0, 0, 0, 0)
        self.frame.setFrameStyle(self.frame.NoFrame | self.frame.Plain)

        super(EbookViewer, self).showFullScreen()

    def show_full_screen_label(self):
        f = self.full_screen_label
        self.esc_full_screen_action.setEnabled(True)
        height = 200
        width = int(0.7 * self.view.width())
        f.resize(width, height)
        f.move((self.view.width() - width) // 2,
               (self.view.height() - height) // 2)
        if self.view.document.show_fullscreen_help:
            f.setVisible(True)
            a = self.full_screen_label_anim
            a.setDuration(500)
            a.setStartValue(QSize(width, 0))
            a.setEndValue(QSize(width, height))
            a.start()
            QTimer.singleShot(3500, self.full_screen_label.hide)
        self.view.document.switch_to_fullscreen_mode()
        if self.view.document.fullscreen_clock:
            self.show_clock()
        if self.view.document.fullscreen_pos:
            self.show_pos_label()

    def show_clock(self):
        self.clock_label.setVisible(True)
        self.clock_label.setText(
            QTime(22, 33, 33).toString(Qt.SystemLocaleShortDate))
        self.clock_timer.start(1000)
        self.clock_label.setStyleSheet(
            self.info_label_style %
            ('rgba(0, 0, 0, 0)', self.view.document.colors()[1]))
        self.clock_label.resize(self.clock_label.sizeHint())
        sw = QApplication.desktop().screenGeometry(self.view)
        vswidth = (self.vertical_scrollbar.width()
                   if self.vertical_scrollbar.isVisible() else 0)
        self.clock_label.move(
            sw.width() - vswidth - 15 - self.clock_label.width(),
            sw.height() - self.clock_label.height() - 10)
        self.update_clock()

    def show_pos_label(self):
        self.pos_label.setVisible(True)
        self.pos_label.setStyleSheet(
            self.info_label_style %
            ('rgba(0, 0, 0, 0)', self.view.document.colors()[1]))
        sw = QApplication.desktop().screenGeometry(self.view)
        self.pos_label.move(15, sw.height() - self.pos_label.height() - 10)
        self.update_pos_label()

    def update_clock(self):
        self.clock_label.setText(QTime.currentTime().toString(
            Qt.SystemLocaleShortDate))

    def update_pos_label(self, *args):
        if self.pos_label.isVisible():
            try:
                value, maximum = args
            except:
                value, maximum = self.pos.value(), self.pos.maximum()
            text = '%g/%g' % (value, maximum)
            self.pos_label.setText(text)
            self.pos_label.resize(self.pos_label.sizeHint())

    def showNormal(self):
        self.view.document.page_position.save()
        self.clock_label.setVisible(False)
        self.pos_label.setVisible(False)
        self.frame.setFrameStyle(self.original_frame_style)
        self.frame.layout().setSpacing(-1)
        self.clock_timer.stop()
        self.vertical_scrollbar.setVisible(True)
        self.window_mode_changed = 'normal'
        self.esc_full_screen_action.setEnabled(False)
        self.settings_changed()
        self.full_screen_label.setVisible(False)
        if hasattr(self, '_original_frame_margins'):
            om = self._original_frame_margins
            self.centralwidget.layout().setContentsMargins(om[0])
            self.frame.layout().setContentsMargins(om[1])
        if self.was_maximized:
            super(EbookViewer, self).showMaximized()
        else:
            super(EbookViewer, self).showNormal()

    def handle_window_mode_toggle(self):
        if self.window_mode_changed:
            fs = self.window_mode_changed == 'fullscreen'
            self.window_mode_changed = None
            if fs:
                self.show_full_screen_label()
            else:
                self.view.document.switch_to_window_mode()
            self.view.document.page_position.restore()
            self.scrolled(self.view.scroll_fraction)

    def goto(self, ref):
        if ref:
            tokens = ref.split('.')
            if len(tokens) > 1:
                spine_index = int(tokens[0]) - 1
                if spine_index == self.current_index:
                    self.view.goto(ref)
                else:
                    self.pending_reference = ref
                    self.load_path(self.iterator.spine[spine_index])

    def goto_bookmark(self, bm):
        spine_index = bm['spine']
        if spine_index > -1 and self.current_index == spine_index:
            if self.resize_in_progress:
                self.view.document.page_position.set_pos(bm['pos'])
            else:
                self.view.goto_bookmark(bm)
        else:
            self.pending_bookmark = bm
            if spine_index < 0 or spine_index >= len(self.iterator.spine):
                spine_index = 0
                self.pending_bookmark = None
            self.load_path(self.iterator.spine[spine_index])

    def toc_clicked(self, index, force=False):
        if force or QApplication.mouseButtons() & Qt.LeftButton:
            item = self.toc_model.itemFromIndex(index)
            if item.abspath is not None:
                if not os.path.exists(item.abspath):
                    return error_dialog(
                        self,
                        _('No such location'),
                        _('The location pointed to by this item'
                          ' does not exist.'),
                        det_msg=item.abspath,
                        show=True)
                url = QUrl.fromLocalFile(item.abspath)
                if item.fragment:
                    url.setFragment(item.fragment)
                self.link_clicked(url)
        self.view.setFocus(Qt.OtherFocusReason)

    def selection_changed(self, selected_text):
        self.selected_text = selected_text.strip()
        self.action_copy.setEnabled(bool(self.selected_text))

    def copy(self, x):
        if self.selected_text:
            QApplication.clipboard().setText(self.selected_text)

    def back(self, x):
        pos = self.history.back(self.pos.value())
        if pos is not None:
            self.goto_page(pos)

    def goto_page_num(self):
        num = self.pos.value()
        self.goto_page(num)

    def forward(self, x):
        pos = self.history.forward(self.pos.value())
        if pos is not None:
            self.goto_page(pos)

    def goto_start(self):
        self.goto_page(1)

    def goto_end(self):
        self.goto_page(self.pos.maximum())

    def goto_page(self, new_page, loaded_check=True):
        if self.current_page is not None or not loaded_check:
            for page in self.iterator.spine:
                if new_page >= page.start_page and new_page <= page.max_page:
                    try:
                        frac = float(new_page -
                                     page.start_page) / (page.pages - 1)
                    except ZeroDivisionError:
                        frac = 0
                    if page == self.current_page:
                        self.view.scroll_to(frac)
                    else:
                        self.load_path(page, pos=frac)

    def open_ebook(self, checked):
        files = choose_files(self,
                             'ebook viewer open dialog',
                             _('Choose ebook'),
                             [(_('Ebooks'), available_input_formats())],
                             all_files=False,
                             select_only_single_file=True)
        if files:
            self.load_ebook(files[0])

    def open_recent(self, action):
        self.load_ebook(action.path)

    def font_size_larger(self):
        self.view.magnify_fonts()

    def font_size_smaller(self):
        self.view.shrink_fonts()

    def magnification_changed(self, val):
        tt = '%(action)s [%(sc)s]\n' + _('Current magnification: %(mag).1f')
        sc = unicode(self.action_font_size_larger.shortcut().toString())
        self.action_font_size_larger.setToolTip(
            tt % dict(action=unicode(self.action_font_size_larger.text()),
                      mag=val,
                      sc=sc))
        sc = unicode(self.action_font_size_smaller.shortcut().toString())
        self.action_font_size_smaller.setToolTip(
            tt % dict(action=unicode(self.action_font_size_smaller.text()),
                      mag=val,
                      sc=sc))
        self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
        self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)

    def find(self, text, repeat=False, backwards=False):
        if not text:
            self.view.search('')
            return self.search.search_done(False)
        if self.view.search(text, backwards=backwards):
            self.scrolled(self.view.scroll_fraction)
            return self.search.search_done(True)
        index = self.iterator.search(text,
                                     self.current_index,
                                     backwards=backwards)
        if index is None:
            if self.current_index > 0:
                index = self.iterator.search(text, 0)
                if index is None:
                    info_dialog(self, _('No matches found'),
                                _('No matches found for: %s') % text).exec_()
                    return self.search.search_done(True)
            return self.search.search_done(True)
        self.pending_search = text
        self.pending_search_dir = 'backwards' if backwards else 'forwards'
        self.load_path(self.iterator.spine[index])

    def find_next(self):
        self.find(unicode(self.search.text()), repeat=True)

    def find_previous(self):
        self.find(unicode(self.search.text()), repeat=True, backwards=True)

    def do_search(self, text, backwards):
        self.pending_search = None
        self.pending_search_dir = None
        if self.view.search(text, backwards=backwards):
            self.scrolled(self.view.scroll_fraction)

    def internal_link_clicked(self, frac):
        self.history.add(self.pos.value())

    def link_clicked(self, url):
        path = os.path.abspath(unicode(url.toLocalFile()))
        frag = None
        if path in self.iterator.spine:
            self.history.add(self.pos.value())
            path = self.iterator.spine[self.iterator.spine.index(path)]
            if url.hasFragment():
                frag = unicode(url.fragment())
            if path != self.current_page:
                self.pending_anchor = frag
                self.load_path(path)
            else:
                oldpos = self.view.document.ypos
                if frag:
                    self.view.scroll_to(frag)
                else:
                    # Scroll to top
                    self.view.scroll_to(0)
                if self.view.document.ypos == oldpos:
                    # If we are coming from goto_next_section() call this will
                    # cause another goto next section call with the next toc
                    # entry, since this one did not cause any scrolling at all.
                    QTimer.singleShot(10, self.update_indexing_state)
        else:
            open_url(url)

    def load_started(self):
        self.open_progress_indicator(_('Loading flow...'))

    def load_finished(self, ok):
        self.close_progress_indicator()
        path = self.view.path()
        try:
            index = self.iterator.spine.index(path)
        except (ValueError, AttributeError):
            return -1
        self.current_page = self.iterator.spine[index]
        self.current_index = index
        self.set_page_number(self.view.scroll_fraction)
        QTimer.singleShot(100, self.update_indexing_state)
        if self.pending_search is not None:
            self.do_search(self.pending_search,
                           self.pending_search_dir == 'backwards')
            self.pending_search = None
            self.pending_search_dir = None
        if self.pending_anchor is not None:
            self.view.scroll_to(self.pending_anchor)
            self.pending_anchor = None
        if self.pending_reference is not None:
            self.view.goto(self.pending_reference)
            self.pending_reference = None
        if self.pending_bookmark is not None:
            self.goto_bookmark(self.pending_bookmark)
            self.pending_bookmark = None
        if self.pending_restore:
            self.view.document.page_position.restore()
        return self.current_index

    def goto_next_section(self):
        if hasattr(self, 'current_index'):
            entry = self.toc_model.next_entry(
                self.current_index, self.view.document.read_anchor_positions(),
                self.view.viewport_rect, self.view.document.in_paged_mode)
            if entry is not None:
                self.pending_goto_next_section = (
                    self.toc_model.currently_viewed_entry, entry, False)
                self.toc_clicked(entry.index(), force=True)

    def goto_previous_section(self):
        if hasattr(self, 'current_index'):
            entry = self.toc_model.next_entry(
                self.current_index,
                self.view.document.read_anchor_positions(),
                self.view.viewport_rect,
                self.view.document.in_paged_mode,
                backwards=True)
            if entry is not None:
                self.pending_goto_next_section = (
                    self.toc_model.currently_viewed_entry, entry, True)
                self.toc_clicked(entry.index(), force=True)

    def update_indexing_state(self, anchor_positions=None):
        pgns = getattr(self, 'pending_goto_next_section', None)
        if hasattr(self, 'current_index'):
            if anchor_positions is None:
                anchor_positions = self.view.document.read_anchor_positions()
            items = self.toc_model.update_indexing_state(
                self.current_index, self.view.viewport_rect, anchor_positions,
                self.view.document.in_paged_mode)
            if items:
                self.toc.scrollTo(items[-1].index())
            if pgns is not None:
                self.pending_goto_next_section = None
                # Check that we actually progressed
                if pgns[0] is self.toc_model.currently_viewed_entry:
                    entry = self.toc_model.next_entry(
                        self.current_index,
                        self.view.document.read_anchor_positions(),
                        self.view.viewport_rect,
                        self.view.document.in_paged_mode,
                        backwards=pgns[2],
                        current_entry=pgns[1])
                    if entry is not None:
                        self.pending_goto_next_section = (
                            self.toc_model.currently_viewed_entry, entry,
                            pgns[2])
                        self.toc_clicked(entry.index(), force=True)

    def load_path(self, path, pos=0.0):
        self.open_progress_indicator(_('Laying out %s') % self.current_title)
        self.view.load_path(path, pos=pos)

    def viewport_resize_started(self, event):
        old, curr = event.size(), event.oldSize()
        if not self.window_mode_changed and old.width() == curr.width():
            # No relayout changes, so page position does not need to be saved
            # This is needed as Qt generates a viewport resized event that
            # changes only the height after a file has been loaded. This can
            # cause the last read position bookmark to become slightly
            # inaccurate
            return
        if not self.resize_in_progress:
            # First resize, so save the current page position
            self.resize_in_progress = True
            if not self.window_mode_changed:
                # The special handling for window mode changed will already
                # have saved page position, so only save it if this is not a
                # mode change
                self.view.document.page_position.save()

        if self.resize_in_progress:
            self.view_resized_timer.start(75)

    def viewport_resize_finished(self):
        # There hasn't been a resize event for some time
        # restore the current page position.
        self.resize_in_progress = False
        if self.window_mode_changed:
            # This resize is part of a window mode change, special case it
            self.handle_window_mode_toggle()
        else:
            self.view.document.page_position.restore()
        self.view.document.after_resize()

    def close_progress_indicator(self):
        self.pi.stop()
        for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar',
                  'vertical_scrollbar'):
            getattr(self, o).setEnabled(True)
        self.unsetCursor()
        self.view.setFocus(Qt.PopupFocusReason)

    def open_progress_indicator(self, msg=''):
        self.pi.start(msg)
        for o in ('tool_bar', 'tool_bar2', 'view', 'horizontal_scrollbar',
                  'vertical_scrollbar'):
            getattr(self, o).setEnabled(False)
        self.setCursor(Qt.BusyCursor)

    def load_theme_menu(self):
        from calibre.gui2.viewer.config import load_themes
        self.themes_menu.clear()
        for key in load_themes():
            title = key[len('theme_'):]
            self.themes_menu.addAction(title, partial(self.load_theme, key))

    def load_theme(self, theme_id):
        self.view.load_theme(theme_id)

    def do_config(self):
        self.view.config(self)
        self.load_theme_menu()
        from calibre.gui2 import config
        if not config['viewer_search_history']:
            self.search.clear_history()

    def bookmark(self, *args):
        num = 1
        bm = None
        while True:
            bm = _('Bookmark #%d') % num
            if bm not in self.existing_bookmarks:
                break
            num += 1
        title, ok = QInputDialog.getText(self,
                                         _('Add bookmark'),
                                         _('Enter title for bookmark:'),
                                         text=bm)
        title = unicode(title).strip()
        if ok and title:
            bm = self.view.bookmark()
            bm['spine'] = self.current_index
            bm['title'] = title
            self.iterator.add_bookmark(bm)
            self.set_bookmarks(self.iterator.bookmarks)

    def set_bookmarks(self, bookmarks):
        self.bookmarks_menu.clear()
        self.bookmarks_menu.addAction(_("Manage Bookmarks"),
                                      self.manage_bookmarks)
        self.bookmarks_menu.addSeparator()
        current_page = None
        self.existing_bookmarks = []
        for bm in bookmarks:
            if bm['title'] == 'calibre_current_page_bookmark':
                if self.get_remember_current_page_opt():
                    current_page = bm
            else:
                self.existing_bookmarks.append(bm['title'])
                self.bookmarks_menu.addAction(bm['title'],
                                              partial(self.goto_bookmark, bm))
        return current_page

    def manage_bookmarks(self):
        bmm = BookmarkManager(self, self.iterator.bookmarks)
        if bmm.exec_() != BookmarkManager.Accepted:
            return

        bookmarks = bmm.get_bookmarks()

        if bookmarks != self.iterator.bookmarks:
            self.iterator.set_bookmarks(bookmarks)
            self.iterator.save_bookmarks()
            self.set_bookmarks(bookmarks)

    def save_current_position(self):
        if not self.get_remember_current_page_opt():
            return
        if hasattr(self, 'current_index'):
            try:
                bm = self.view.bookmark()
                bm['spine'] = self.current_index
                bm['title'] = 'calibre_current_page_bookmark'
                self.iterator.add_bookmark(bm)
            except:
                traceback.print_exc()

    def load_ebook(self, pathtoebook, open_at=None):
        if self.iterator is not None:
            self.save_current_position()
            self.iterator.__exit__()
        self.iterator = EbookIterator(pathtoebook)
        self.open_progress_indicator(_('Loading ebook...'))
        worker = Worker(target=partial(self.iterator.__enter__,
                                       extract_embedded_fonts_for_qt=True))
        worker.start()
        while worker.isAlive():
            worker.join(0.1)
            QApplication.processEvents()
        if worker.exception is not None:
            if isinstance(worker.exception, DRMError):
                from calibre.gui2.dialogs.drm_error import DRMErrorMessage
                DRMErrorMessage(self).exec_()
            else:
                r = getattr(worker.exception, 'reason', worker.exception)
                error_dialog(self,
                             _('Could not open ebook'),
                             as_unicode(r) or _('Unknown error'),
                             det_msg=worker.traceback,
                             show=True)
            self.close_progress_indicator()
        else:
            self.metadata.show_opf(self.iterator.opf,
                                   self.iterator.book_format)
            self.view.current_language = self.iterator.language
            title = self.iterator.opf.title
            if not title:
                title = os.path.splitext(os.path.basename(pathtoebook))[0]
            if self.iterator.toc:
                self.toc_model = TOC(self.iterator.spine, self.iterator.toc)
                self.toc.setModel(self.toc_model)
                if self.show_toc_on_open:
                    self.action_table_of_contents.setChecked(True)
            else:
                self.toc_model = TOC(self.iterator.spine)
                self.toc.setModel(self.toc_model)
                self.action_table_of_contents.setChecked(False)
            if isbytestring(pathtoebook):
                pathtoebook = force_unicode(pathtoebook, filesystem_encoding)
            vh = vprefs.get('viewer_open_history', [])
            try:
                vh.remove(pathtoebook)
            except:
                pass
            vh.insert(0, pathtoebook)
            vprefs.set('viewer_open_history', vh[:50])
            self.build_recent_menu()

            self.action_table_of_contents.setDisabled(not self.iterator.toc)
            self.current_book_has_toc = bool(self.iterator.toc)
            self.current_title = title
            self.setWindowTitle(self.base_window_title + ' - ' + title +
                                ' [%s]' % self.iterator.book_format)
            self.pos.setMaximum(sum(self.iterator.pages))
            self.pos.setSuffix(' / %d' % sum(self.iterator.pages))
            self.vertical_scrollbar.setMinimum(100)
            self.vertical_scrollbar.setMaximum(100 * sum(self.iterator.pages))
            self.vertical_scrollbar.setSingleStep(10)
            self.vertical_scrollbar.setPageStep(100)
            self.set_vscrollbar_value(1)
            self.current_index = -1
            QApplication.instance().alert(self, 5000)
            previous = self.set_bookmarks(self.iterator.bookmarks)
            if open_at is None and previous is not None:
                self.goto_bookmark(previous)
            else:
                if open_at is None:
                    self.next_document()
                else:
                    if open_at > self.pos.maximum():
                        open_at = self.pos.maximum()
                    if open_at < self.pos.minimum():
                        open_at = self.pos.minimum()
                    self.goto_page(open_at, loaded_check=False)

    def set_vscrollbar_value(self, pagenum):
        self.vertical_scrollbar.blockSignals(True)
        self.vertical_scrollbar.setValue(int(pagenum * 100))
        self.vertical_scrollbar.blockSignals(False)

    def set_page_number(self, frac):
        if getattr(self, 'current_page', None) is not None:
            page = self.current_page.start_page + frac * float(
                self.current_page.pages - 1)
            self.pos.set_value(page)
            self.set_vscrollbar_value(page)

    def scrolled(self, frac, onload=False):
        self.set_page_number(frac)
        if not onload:
            ap = self.view.document.read_anchor_positions()
            self.update_indexing_state(ap)

    def next_document(self):
        if (hasattr(self, 'current_index')
                and self.current_index < len(self.iterator.spine) - 1):
            self.load_path(self.iterator.spine[self.current_index + 1])

    def previous_document(self):
        if hasattr(self, 'current_index') and self.current_index > 0:
            self.load_path(self.iterator.spine[self.current_index - 1],
                           pos=1.0)

    def keyPressEvent(self, event):
        MainWindow.keyPressEvent(self, event)
        if not event.isAccepted():
            if not self.view.handle_key_press(event):
                event.ignore()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        if self.iterator is not None:
            self.save_current_position()
            self.iterator.__exit__(*args)

    def read_settings(self):
        c = config().parse()
        self.splitter.setSizes([1, 300])
        if c.remember_window_size:
            wg = vprefs.get('viewer_window_geometry', None)
            if wg is not None:
                self.restoreGeometry(wg)
            ss = vprefs.get('viewer_splitter_state', None)
            if ss is not None:
                self.splitter.restoreState(ss)
            self.show_toc_on_open = vprefs.get('viewer_toc_isvisible', False)
        av = available_height() - 30
        if self.height() > av:
            self.resize(self.width(), av)
        self.splitter.setCollapsible(0, False)
        self.splitter.setCollapsible(1, False)
Exemple #51
0
    def show_context_menu(self, point):
        item = self.itemAt(point)
        if item is None or item in set(self.categories.itervalues()):
            return
        m = QMenu(self)
        sel = self.selectedItems()
        num = len(sel)
        if num > 0:
            m.addAction(QIcon(I('trash.png')), _('&Delete selected files'),
                        self.request_delete)
        ci = self.currentItem()
        if ci is not None:
            cn = unicode(ci.data(0, NAME_ROLE).toString())
            mt = unicode(ci.data(0, MIME_ROLE).toString())
            cat = unicode(ci.data(0, CATEGORY_ROLE).toString())
            m.addAction(QIcon(I('modified.png')),
                        _('&Rename %s') % (elided_text(self.font(), cn)),
                        self.edit_current_item)
            if is_raster_image(mt):
                m.addAction(
                    QIcon(I('default_cover.png')),
                    _('Mark %s as cover image') % elided_text(self.font(), cn),
                    partial(self.mark_as_cover, cn))
            elif current_container(
            ).SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text':
                m.addAction(
                    QIcon(I('default_cover.png')),
                    _('Mark %s as title/cover page') %
                    elided_text(self.font(), cn),
                    partial(self.mark_as_titlepage, cn))

        selected_map = defaultdict(list)
        for item in sel:
            selected_map[unicode(item.data(
                0, CATEGORY_ROLE).toString())].append(
                    unicode(item.data(0, NAME_ROLE).toString()))

        for items in selected_map.itervalues():
            items.sort(key=self.index_of_name)

        if len(selected_map['text']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected text files'),
                partial(self.start_merge, 'text', selected_map['text']))
        if len(selected_map['styles']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected style files'),
                partial(self.start_merge, 'styles', selected_map['styles']))

        if len(list(m.actions())) > 0:
            m.popup(self.mapToGlobal(point))
Exemple #52
0
class ChooseLibraryAction(InterfaceAction):

    name = 'Choose Library'
    action_spec = (_('Choose Library'), 'lt.png',
                   _('Choose calibre library to work with'), None)
    dont_add_to = frozenset(['context-menu-device'])
    action_add_menu = True
    action_menu_clone_qaction = _('Switch/create library...')
    restore_view_state = pyqtSignal(object)

    def genesis(self):
        self.count_changed(0)
        self.action_choose = self.menuless_qaction

        self.stats = LibraryUsageStats()
        self.popup_type = (QToolButton.InstantPopup
                           if len(self.stats.stats) > 1 else
                           QToolButton.MenuButtonPopup)
        if len(self.stats.stats) > 1:
            self.action_choose.triggered.connect(self.choose_library)
        else:
            self.qaction.triggered.connect(self.choose_library)

        self.choose_menu = self.qaction.menu()

        ac = self.create_action(spec=(_('Pick a random book'), 'random.png',
                                      None, None),
                                attr='action_pick_random')
        ac.triggered.connect(self.pick_random)

        if not os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            self.choose_menu.addAction(self.action_choose)

            self.quick_menu = QMenu(_('Quick switch'))
            self.quick_menu_action = self.choose_menu.addMenu(self.quick_menu)
            self.rename_menu = QMenu(_('Rename library'))
            self.rename_menu_action = self.choose_menu.addMenu(
                self.rename_menu)
            self.choose_menu.addAction(ac)
            self.delete_menu = QMenu(_('Remove library'))
            self.delete_menu_action = self.choose_menu.addMenu(
                self.delete_menu)
        else:
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.switch_actions = []
        for i in range(5):
            ac = self.create_action(spec=('', None, None, None),
                                    attr='switch_action%d' % i)
            self.switch_actions.append(ac)
            ac.setVisible(False)
            ac.triggered.connect(partial(self.qs_requested, i),
                                 type=Qt.QueuedConnection)
            self.choose_menu.addAction(ac)

        self.rename_separator = self.choose_menu.addSeparator()

        self.maintenance_menu = QMenu(_('Library Maintenance'))
        ac = self.create_action(spec=(_('Library metadata backup status'),
                                      'lt.png', None, None),
                                attr='action_backup_status')
        ac.triggered.connect(self.backup_status, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Check library'), 'lt.png', None,
                                      None),
                                attr='action_check_library')
        ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)
        ac = self.create_action(spec=(_('Restore database'), 'lt.png', None,
                                      None),
                                attr='action_restore_database')
        ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
        self.maintenance_menu.addAction(ac)

        self.choose_menu.addMenu(self.maintenance_menu)
        self.view_state_map = {}
        self.restore_view_state.connect(self._restore_view_state,
                                        type=Qt.QueuedConnection)

    @property
    def preserve_state_on_switch(self):
        ans = getattr(self, '_preserve_state_on_switch', None)
        if ans is None:
            self._preserve_state_on_switch = ans = \
                self.gui.library_view.preserve_state(require_selected_ids=False)
        return ans

    def pick_random(self, *args):
        self.gui.iactions['Pick Random Book'].pick_random()

    def library_name(self):
        db = self.gui.library_view.model().db
        path = db.library_path
        if isbytestring(path):
            path = path.decode(filesystem_encoding)
        path = path.replace(os.sep, '/')
        return self.stats.pretty(path)

    def update_tooltip(self, count):
        tooltip = self.action_spec[2] + '\n\n' + _('{0} [{1} books]').format(
            getattr(self, 'last_lname', ''), count)
        a = self.qaction
        a.setToolTip(tooltip)
        a.setStatusTip(tooltip)
        a.setWhatsThis(tooltip)

    def library_changed(self, db):
        lname = self.stats.library_used(db)
        self.last_lname = lname
        if len(lname) > 16:
            lname = lname[:16] + u'…'
        a = self.qaction
        a.setText(lname)
        self.update_tooltip(db.count())
        self.build_menus()
        state = self.view_state_map.get(
            self.stats.canonicalize_path(db.library_path), None)
        if state is not None:
            self.restore_view_state.emit(state)

    def _restore_view_state(self, state):
        self.preserve_state_on_switch.state = state

    def initialization_complete(self):
        self.library_changed(self.gui.library_view.model().db)

    def build_menus(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            return
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))

        for ac in self.switch_actions:
            ac.setVisible(False)
        self.quick_menu.clear()
        self.qs_locations = [i[1] for i in locations]
        self.rename_menu.clear()
        self.delete_menu.clear()
        quick_actions, rename_actions, delete_actions = [], [], []
        for name, loc in locations:
            ac = self.quick_menu.addAction(
                name, Dispatcher(partial(self.switch_requested, loc)))
            quick_actions.append(ac)
            ac = self.rename_menu.addAction(
                name, Dispatcher(partial(self.rename_requested, name, loc)))
            rename_actions.append(ac)
            ac = self.delete_menu.addAction(
                name, Dispatcher(partial(self.delete_requested, name, loc)))
            delete_actions.append(ac)

        qs_actions = []
        for i, x in enumerate(locations[:len(self.switch_actions)]):
            name, loc = x
            ac = self.switch_actions[i]
            ac.setText(name)
            ac.setVisible(True)
            qs_actions.append(ac)

        self.quick_menu_action.setVisible(bool(locations))
        self.rename_menu_action.setVisible(bool(locations))
        self.delete_menu_action.setVisible(bool(locations))
        self.gui.location_manager.set_switch_actions(quick_actions,
                                                     rename_actions,
                                                     delete_actions,
                                                     qs_actions,
                                                     self.action_choose)

    def location_selected(self, loc):
        enabled = loc == 'library'
        self.qaction.setEnabled(enabled)

    def rename_requested(self, name, location):
        LibraryDatabase = db_class()
        loc = location.replace('/', os.sep)
        base = os.path.dirname(loc)
        newname, ok = QInputDialog.getText(
            self.gui,
            _('Rename') + ' ' + name,
            '<p>' + _('Choose a new name for the library <b>%s</b>. ') % name +
            '<p>' + _('Note that the actual library folder will be renamed.'),
            text=name)
        newname = sanitize_file_name_unicode(unicode(newname))
        if not ok or not newname or newname == name:
            return
        newloc = os.path.join(base, newname)
        if os.path.exists(newloc):
            return error_dialog(
                self.gui,
                _('Already exists'),
                _('The folder %s already exists. Delete it first.') % newloc,
                show=True)
        if (iswindows
                and len(newloc) > LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(self.gui,
                                _('Too long'),
                                _('Path to library too long. Must be less than'
                                  ' %d characters.') %
                                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                                show=True)
        if not os.path.exists(loc):
            error_dialog(
                self.gui,
                _('Not found'),
                _('Cannot rename as no library was found at %s. '
                  'Try switching to this library first, then switch back '
                  'and retry the renaming.') % loc,
                show=True)
            return
        try:
            os.rename(loc, newloc)
        except:
            import traceback
            det_msg = 'Location: %r New Location: %r\n%s' % (
                loc, newloc, traceback.format_exc())
            error_dialog(
                self.gui,
                _('Rename failed'),
                _('Failed to rename the library at %s. '
                  'The most common cause for this is if one of the files'
                  ' in the library is open in another program.') % loc,
                det_msg=det_msg,
                show=True)
            return
        self.stats.rename(location, newloc)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def delete_requested(self, name, location):
        loc = location.replace('/', os.sep)
        self.stats.remove(location)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()
        info_dialog(self.gui,
                    _('Library removed'),
                    _('The library %s has been removed from calibre. '
                      'The files remain on your computer, if you want '
                      'to delete them, you will have to do so manually.') %
                    loc,
                    show=True)
        if os.path.exists(loc):
            open_local_file(loc)

    def backup_status(self, location):
        self.__backup_status_dialog = d = BackupStatus(self.gui)
        d.show()

    def mark_dirty(self):
        db = self.gui.library_view.model().db
        db.dirtied(list(db.data.iterallids()))
        info_dialog(
            self.gui,
            _('Backup metadata'),
            _('Metadata will be backed up while calibre is running, at the '
              'rate of approximately 1 book every three seconds.'),
            show=True)

    def restore_database(self):
        LibraryDatabase = db_class()
        m = self.gui.library_view.model()
        db = m.db
        if (iswindows and len(db.library_path) >
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT):
            return error_dialog(
                self.gui,
                _('Too long'),
                _('Path to library too long. Must be less than'
                  ' %d characters. Move your library to a location with'
                  ' a shorter path using Windows Explorer, then point'
                  ' calibre to the new location and try again.') %
                LibraryDatabase.WINDOWS_LIBRARY_PATH_LIMIT,
                show=True)

        from calibre.gui2.dialogs.restore_library import restore_database
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True
        if restore_database(db, self.gui):
            self.gui.library_moved(db.library_path, call_close=False)

    def check_library(self):
        from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
        self.gui.library_view.save_state()
        m = self.gui.library_view.model()
        m.stop_metadata_backup()
        db = m.db
        db.prefs.disable_setting = True

        d = DBCheck(self.gui, db)
        d.start()
        try:
            d.conn.close()
        except:
            pass
        d.break_cycles()
        self.gui.library_moved(db.library_path,
                               call_close=not d.closed_orig_conn)
        if d.rejected:
            return
        if d.error is None:
            if not question_dialog(
                    self.gui, _('Success'),
                    _('Found no errors in your calibre library database.'
                      ' Do you want calibre to check if the files in your '
                      ' library match the information in the database?')):
                return
        else:
            return error_dialog(
                self.gui,
                _('Failed'),
                _('Database integrity check failed, click Show details'
                  ' for details.'),
                show=True,
                det_msg=d.error[1])

        self.gui.status_bar.show_message(
            _('Starting library scan, this may take a while'))
        try:
            QCoreApplication.processEvents()
            d = CheckLibraryDialog(self.gui, m.db)

            if not d.do_exec():
                info_dialog(
                    self.gui,
                    _('No problems found'),
                    _('The files in your library match the information '
                      'in the database.'),
                    show=True)
        finally:
            self.gui.status_bar.clear_message()

    def look_for_portable_lib(self, db, location):
        base = get_portable_base()
        if base is None:
            return False, None
        loc = location.replace('/', os.sep)
        candidate = os.path.join(base, os.path.basename(loc))
        if db.exists_at(candidate):
            newloc = candidate.replace(os.sep, '/')
            self.stats.rename(location, newloc)
            return True, newloc
        return False, None

    def switch_requested(self, location):
        if not self.change_library_allowed():
            return
        db = self.gui.library_view.model().db
        current_lib = self.stats.canonicalize_path(db.library_path)
        self.view_state_map[current_lib] = self.preserve_state_on_switch.state
        loc = location.replace('/', os.sep)
        exists = db.exists_at(loc)
        if not exists:
            exists, new_location = self.look_for_portable_lib(db, location)
            if exists:
                location = new_location
                loc = location.replace('/', os.sep)

        if not exists:
            d = MovedDialog(self.stats, location, self.gui)
            ret = d.exec_()
            self.build_menus()
            self.gui.iactions['Copy To Library'].build_menus()
            if ret == d.Accepted:
                loc = d.newloc.replace('/', os.sep)
            else:
                return

        # from calibre.utils.mem import memory
        # import weakref
        # from PyQt4.Qt import QTimer
        # self.dbref = weakref.ref(self.gui.library_view.model().db)
        # self.before_mem = memory()
        self.gui.library_moved(loc, allow_rebuild=True)
        # QTimer.singleShot(5000, self.debug_leak)

    def debug_leak(self):
        import gc
        from calibre.utils.mem import memory
        ref = self.dbref
        for i in xrange(3):
            gc.collect()
        if ref() is not None:
            print 'DB object alive:', ref()
            for r in gc.get_referrers(ref())[:10]:
                print r
                print
        print 'before:', self.before_mem
        print 'after:', memory()
        print
        self.dbref = self.before_mem = None

    def qs_requested(self, idx, *args):
        self.switch_requested(self.qs_locations[idx])

    def count_changed(self, new_count):
        self.update_tooltip(new_count)

    def choose_library(self, *args):
        if not self.change_library_allowed():
            return
        from calibre.gui2.dialogs.choose_library import ChooseLibrary
        self.gui.library_view.save_state()
        db = self.gui.library_view.model().db
        location = self.stats.canonicalize_path(db.library_path)
        self.pre_choose_dialog_location = location
        c = ChooseLibrary(db, self.choose_library_callback, self.gui)
        c.exec_()
        self.choose_dialog_library_renamed = getattr(c, 'library_renamed',
                                                     False)

    def choose_library_callback(self, newloc, copy_structure=False):
        self.gui.library_moved(newloc,
                               copy_structure=copy_structure,
                               allow_rebuild=True)
        if getattr(self, 'choose_dialog_library_renamed', False):
            self.stats.rename(self.pre_choose_dialog_location,
                              prefs['library_path'])
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()

    def change_library_allowed(self):
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            warning_dialog(
                self.gui,
                _('Not allowed'),
                _('You cannot change libraries while using the environment'
                  ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'),
                show=True)
            return False
        if self.gui.job_manager.has_jobs():
            warning_dialog(self.gui,
                           _('Not allowed'),
                           _('You cannot change libraries while jobs'
                             ' are running.'),
                           show=True)
            return False

        return True
Exemple #53
0
    def __init__(self, parent, hide_on_close=False):
        QMainWindow.__init__(self, parent)
        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.setIcon(pixmaps.purr_logo.pm())
        cw = QWidget(self)
        self.setCentralWidget(cw)
        cwlo = QVBoxLayout(cw)
        cwlo.setMargin(5)
        # make splitter
        spl1 = self._splitter1 = QSplitter(Qt.Vertical, 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()
        QObject.connect(self.skyplot, SIGNAL("imagesChanged"), self._imagesChanged)
        QObject.connect(self.skyplot, SIGNAL("showMessage"), self.showMessage)
        QObject.connect(self.skyplot, SIGNAL("showErrorMessage"), self.showErrorMessage)

        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)
        QObject.connect(self, SIGNAL("hasSkyModel"), 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.skyplot.setImageManager(self.imgman)
        QObject.connect(self.imgman, SIGNAL("imagesChanged"), self._imagesChanged)
        QObject.connect(self.imgman, SIGNAL("showMessage"), self.showMessage)
        QObject.connect(self.imgman, SIGNAL("showErrorMessage"), self.showErrorMessage)

        # 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)
        QObject.connect(self, SIGNAL("hasSkyModel"), qa_merge.setEnabled)
        file_menu.addSeparator()
        qa_save = file_menu.addAction("&Save model", self.saveFile, Qt.CTRL + Qt.Key_S)
        QObject.connect(self, SIGNAL("isUpdated"), qa_save.setEnabled)
        qa_save_as = file_menu.addAction("Save model &as...", self.saveFileAs)
        QObject.connect(self, SIGNAL("hasSkyModel"), qa_save_as.setEnabled)
        qa_save_selection_as = file_menu.addAction("Save selection as...", self.saveSelectionAs)
        QObject.connect(self, SIGNAL("hasSelection"), qa_save_selection_as.setEnabled)
        file_menu.addSeparator()
        qa_close = file_menu.addAction("&Close model", self.closeFile, Qt.CTRL + Qt.Key_W)
        QObject.connect(self, SIGNAL("hasSkyModel"), 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)
        QObject.connect(self, SIGNAL("hasSkyModel"), 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("&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)
        QObject.connect(self, SIGNAL("hasSelection"), qa_add_tag.setEnabled)
        qa_del_tag = em.addAction("&Untag selection...", self.removeTagsFromSelection, Qt.CTRL + Qt.Key_U)
        QObject.connect(self, SIGNAL("hasSelection"), qa_del_tag.setEnabled)
        qa_del_sel = em.addAction("&Delete selection", self._deleteSelection)
        QObject.connect(self, SIGNAL("hasSelection"), qa_del_sel.setEnabled)

        # Tools menu
        tm = self._tools_menu = QMenu("&Tools", self)
        self._qa_tm = menubar.addMenu(tm)
        self._qa_tm.setVisible(False)
        QObject.connect(self, SIGNAL("hasSkyModel"), 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.emit(SIGNAL("isUpdated"), False)
        self.emit(SIGNAL("hasSkyModel"), False)
        self.emit(SIGNAL("hasSelection"), False)
        self._exiting = False

        # set initial layout
        self._current_layout = None
        self.setLayout(self.LayoutEmpty)
        dprint(1, "init complete")
Exemple #54
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.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.setMovable(True)
        self.column_header.setClickable(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.setResizeMode(self.row_header.Fixed)
        self.setVerticalHeader(self.row_header)
        # }}}

        self._model.database_changed.connect(self.database_changed)
        hv = self.verticalHeader()
        hv.setClickable(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':
            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).toString())
            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).toString())
                    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):
        m = self._model
        self.column_header.setSectionHidden(m.column_map.index('ondevice'),
                                            not m.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.prefs.set(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)

        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.prefs[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.prefs[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:
            self.resizeRowToContents(0)
            self.verticalHeader().setDefaultSectionSize(
                self.rowHeight(0) + gprefs['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))
        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()
        #}}}

    # 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 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)
Exemple #55
0
class ShareConnMenu(QMenu):  # {{{

    connect_to_folder = pyqtSignal()
    connect_to_itunes = pyqtSignal()
    connect_to_bambook = pyqtSignal()

    config_email = pyqtSignal()
    toggle_server = pyqtSignal()
    dont_add_to = frozenset(['context-menu-device'])

    def __init__(self, parent=None):
        QMenu.__init__(self, parent)
        mitem = self.addAction(QIcon(I('devices/folder.png')),
                               _('Connect to folder'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x: self.connect_to_folder.emit())
        self.connect_to_folder_action = mitem
        mitem = self.addAction(QIcon(I('devices/itunes.png')),
                               _('Connect to iTunes'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x: self.connect_to_itunes.emit())
        self.connect_to_itunes_action = mitem
        if not (iswindows or isosx):
            mitem.setVisible(False)
        mitem = self.addAction(QIcon(I('devices/bambook.png')),
                               _('Connect to Bambook'))
        mitem.setEnabled(True)
        mitem.triggered.connect(lambda x: self.connect_to_bambook.emit())
        self.connect_to_bambook_action = mitem
        bambook_visible = False
        if not is_disabled(BAMBOOK):
            device_ip = BAMBOOK.settings().extra_customization
            if device_ip:
                bambook_visible = True
        self.connect_to_bambook_action.setVisible(bambook_visible)

        self.addSeparator()
        self.toggle_server_action = \
            self.addAction(QIcon(I('network-server.png')),
            _('Start Content Server'))
        self.toggle_server_action.triggered.connect(
            lambda x: self.toggle_server.emit())
        self.addSeparator()

        self.email_actions = []

        if hasattr(parent, 'keyboard'):
            r = parent.keyboard.register_shortcut
            prefix = 'Share/Connect Menu '
            gr = ConnectShareAction.action_spec[0]
            for attr in ('folder', 'bambook', 'itunes'):
                if not (iswindows or isosx) and attr == 'itunes':
                    continue
                ac = getattr(self, 'connect_to_%s_action' % attr)
                r(prefix + attr, unicode(ac.text()), action=ac, group=gr)
            r(prefix + ' content server',
              _('Start/stop content server'),
              action=self.toggle_server_action,
              group=gr)

    def server_state_changed(self, running):
        from calibre.utils.mdns import get_external_ip
        text = _('Start Content Server')
        if running:
            text = _('Stop Content Server') + ' [%s]' % get_external_ip()
        self.toggle_server_action.setText(text)

    def build_email_entries(self, sync_menu):
        from calibre.gui2.device import DeviceAction
        for ac in self.email_actions:
            self.removeAction(ac)
        self.email_actions = []
        self.memory = []
        opts = email_config().parse()
        if opts.accounts:
            self.email_to_menu = QMenu(_('Email to') + '...', self)
            ac = self.addMenu(self.email_to_menu)
            self.email_actions.append(ac)
            self.email_to_and_delete_menu = QMenu(
                _('Email to and delete from library') + '...', self)
            keys = sorted(opts.accounts.keys())
            for account in keys:
                formats, auto, default = opts.accounts[account]
                subject = opts.subjects.get(account, '')
                dest = 'mail:' + account + ';' + formats + ';' + subject
                action1 = DeviceAction(dest, False, False, I('mail.png'),
                                       account)
                action2 = DeviceAction(
                    dest, True, False, I('mail.png'),
                    account + ' ' + _('(delete from library)'))
                self.email_to_menu.addAction(action1)
                self.email_to_and_delete_menu.addAction(action2)
                map(self.memory.append, (action1, action2))
                if default:
                    ac = DeviceAction(dest, False, False, I('mail.png'),
                                      _('Email to') + ' ' + account)
                    self.addAction(ac)
                    self.email_actions.append(ac)
                    ac.a_s.connect(sync_menu.action_triggered)
                action1.a_s.connect(sync_menu.action_triggered)
                action2.a_s.connect(sync_menu.action_triggered)
            ac = self.addMenu(self.email_to_and_delete_menu)
            self.email_actions.append(ac)
        else:
            ac = self.addAction(_('Setup email based sharing of books'))
            self.email_actions.append(ac)
            ac.triggered.connect(self.setup_email)

    def setup_email(self, *args):
        self.config_email.emit()

    def set_state(self, device_connected):
        self.connect_to_folder_action.setEnabled(not device_connected)
        self.connect_to_itunes_action.setEnabled(not device_connected)
        self.connect_to_bambook_action.setEnabled(not device_connected)
Exemple #56
0
class ConfigWidget(ConfigWidgetBase, Ui_Form):

    def genesis(self, gui):
        self.gui = gui
        self.delegate = Delegate(self.tweaks_view)
        self.tweaks_view.setItemDelegate(self.delegate)
        self.tweaks_view.currentChanged = self.current_changed
        self.view = self.tweaks_view
        self.highlighter = PythonHighlighter(self.edit_tweak.document())
        self.restore_default_button.clicked.connect(self.restore_to_default)
        self.apply_button.clicked.connect(self.apply_tweak)
        self.plugin_tweaks_button.clicked.connect(self.plugin_tweaks)
        self.splitter.setStretchFactor(0, 1)
        self.splitter.setStretchFactor(1, 100)
        self.next_button.clicked.connect(self.find_next)
        self.previous_button.clicked.connect(self.find_previous)
        self.search.initialize('tweaks_search_history', help_text=_('Search for tweak'))
        self.search.search.connect(self.find)
        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.show_context_menu)
        self.copy_icon = QIcon(I('edit-copy.png'))

    def show_context_menu(self, point):
        idx = self.tweaks_view.currentIndex()
        if not idx.isValid():
            return True
        tweak = self.tweaks.data(idx, Qt.UserRole)
        self.context_menu = QMenu(self)
        self.context_menu.addAction(self.copy_icon,
                            _('Copy to clipboard'),
                            partial(self.copy_item_to_clipboard,
                                    val=u"%s (%s: %s)"%(tweak.name,
                                                        _('ID'),
                                                        tweak.var_names[0])))
        self.context_menu.popup(self.mapToGlobal(point))
        return True

    def copy_item_to_clipboard(self, val):
        cb = QApplication.clipboard()
        cb.clear()
        cb.setText(val)

    def plugin_tweaks(self):
        raw = self.tweaks.plugin_tweaks_string
        d = PluginTweaks(raw, self)
        if d.exec_() == d.Accepted:
            g, l = {}, {}
            try:
                exec(unicode(d.edit.toPlainText()), g, l)
            except:
                import traceback
                return error_dialog(self, _('Failed'),
                    _('There was a syntax error in your tweak. Click '
                        'the show details button for details.'), show=True,
                    det_msg=traceback.format_exc())
            self.tweaks.set_plugin_tweaks(l)
            self.changed()

    def current_changed(self, current, previous):
        self.tweaks_view.scrollTo(current)
        tweak = self.tweaks.data(current, Qt.UserRole)
        self.help.setPlainText(tweak.doc)
        self.edit_tweak.setPlainText(tweak.edit_text)

    def changed(self, *args):
        self.changed_signal.emit()

    def initialize(self):
        self.tweaks = self._model = Tweaks()
        self.tweaks_view.setModel(self.tweaks)

    def restore_to_default(self, *args):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            self.tweaks.restore_to_default(idx)
            tweak = self.tweaks.data(idx, Qt.UserRole)
            self.edit_tweak.setPlainText(tweak.edit_text)
            self.changed()

    def restore_defaults(self):
        ConfigWidgetBase.restore_defaults(self)
        self.tweaks.restore_to_defaults()
        self.changed()

    def apply_tweak(self):
        idx = self.tweaks_view.currentIndex()
        if idx.isValid():
            l, g = {}, {}
            try:
                exec(unicode(self.edit_tweak.toPlainText()), g, l)
            except:
                import traceback
                error_dialog(self.gui, _('Failed'),
                        _('There was a syntax error in your tweak. Click '
                            'the show details button for details.'),
                        det_msg=traceback.format_exc(), show=True)
                return
            self.tweaks.update_tweak(idx, l)
            self.changed()

    def commit(self):
        raw = self.tweaks.to_string()
        try:
            exec(raw)
        except:
            import traceback
            error_dialog(self, _('Invalid tweaks'),
                    _('The tweaks you entered are invalid, try resetting the'
                        ' tweaks to default and changing them one by one until'
                        ' you find the invalid setting.'),
                    det_msg=traceback.format_exc(), show=True)
            raise AbortCommit('abort')
        write_tweaks(raw)
        ConfigWidgetBase.commit(self)
        return True

    def find(self, query):
        if not query:
            return
        try:
            idx = self._model.find(query)
        except ParseException:
            self.search.search_done(False)
            return
        self.search.search_done(True)
        if not idx.isValid():
            info_dialog(self, _('No matches'),
                    _('Could not find any shortcuts matching %s')%query,
                    show=True, show_copy_button=False)
            return
        self.highlight_index(idx)

    def highlight_index(self, idx):
        if not idx.isValid():
            return
        self.view.scrollTo(idx)
        self.view.selectionModel().select(idx,
                self.view.selectionModel().ClearAndSelect)
        self.view.setCurrentIndex(idx)

    def find_next(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
                unicode(self.search.currentText()))
        self.highlight_index(idx)

    def find_previous(self, *args):
        idx = self.view.currentIndex()
        if not idx.isValid():
            idx = self._model.index(0)
        idx = self._model.find_next(idx,
            unicode(self.search.currentText()), backwards=True)
        self.highlight_index(idx)
Exemple #57
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)

    def __init__(self, parent=None):
        QTreeView.__init__(self, parent=None)
        self.alter_tb = None
        self.disable_recounting = False
        self.setUniformRowHeights(True)
        self.setCursor(Qt.PointingHandCursor)
        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.9ex;
                    padding-bottom:0.9ex;
                }

                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)

    @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.prefs.set('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 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(Qt.DecorationRole).toPyObject().pixmap(25, 25)
        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):
        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':
                self.edit(index)
                return
            if action == 'delete_item':
                self.tag_item_delete.emit(key, index.id, index.original_name)
                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':
                saved_searches().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.prefs.set('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).toPyObject()
            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.toString())
                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
                        self.context_menu.addAction(
                            self.rename_icon,
                            _('Rename %s') % display_name(tag),
                            partial(self.context_menu_handler,
                                    action='edit_item',
                                    index=index))
                        if key in ('tags', 'series', 'publisher') or \
                                self._model.db.field_metadata.is_custom_field(key):
                            self.context_menu.addAction(
                                self.delete_icon,
                                _('Delete %s') % display_name(tag),
                                partial(self.context_menu_handler,
                                        action='delete_item',
                                        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',
                                    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',
                                    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 not self.context_menu.isEmpty():
            self.context_menu.popup(self.mapToGlobal(point))
        return True

    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).toPyObject()
        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).toPyObject()
        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).toPyObject() 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)
Exemple #58
0
class ImageController(QFrame):
    """An ImageController is a widget for controlling the display of one image.
    It can emit the following signals from the image:
    raise                     raise button was clicked
    center                  center-on-image option was selected
    unload                  unload option was selected
    slice                     image slice has changed, need to redraw (emitted by SkyImage automatically)
    repaint                 image display range or colormap has changed, need to redraw (emitted by SkyImage automatically)
    """

    def __init__(self, image, parent, imgman, name=None, save=False):
        QFrame.__init__(self, parent)
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
        # init state
        self.image = image
        self._imgman = imgman
        self._currier = PersistentCurrier()
        self._control_dialog = None
        # create widgets
        self._lo = lo = QHBoxLayout(self)
        lo.setContentsMargins(0, 0, 0, 0)
        lo.setSpacing(2)
        # raise button
        self._wraise = QToolButton(self)
        lo.addWidget(self._wraise)
        self._wraise.setIcon(pixmaps.raise_up.icon())
        self._wraise.setAutoRaise(True)
        self._can_raise = False
        QObject.connect(self._wraise, SIGNAL("clicked()"), self._raiseButtonPressed)
        self._wraise.setToolTip("""<P>Click here to raise this image above other images. Hold the button down briefly to
      show a menu of image operations.</P>""")
        # center label
        self._wcenter = QLabel(self)
        self._wcenter.setPixmap(pixmaps.center_image.pm())
        self._wcenter.setToolTip(
            "<P>The plot is currently centered on (the reference pixel %d,%d) of this image.</P>" % self.image.referencePixel())
        lo.addWidget(self._wcenter)
        # name/filename label
        self.name = image.name
        self._wlabel = QLabel(self.name, self)
        self._number = 0
        self.setName(self.name)
        self._wlabel.setToolTip("%s %s" % (image.filename, "\u00D7".join(map(str, image.data().shape))))
        lo.addWidget(self._wlabel, 1)
        # if 'save' is specified, create a "save" button
        if save:
            self._wsave = QToolButton(self)
            lo.addWidget(self._wsave)
            self._wsave.setText("save")
            self._wsave.setAutoRaise(True)
            self._save_dir = save if isinstance(save, str) else "."
            QObject.connect(self._wsave, SIGNAL("clicked()"), self._saveImage)
            self._wsave.setToolTip("""<P>Click here to write this image to a FITS file.</P>""")
        # render control
        dprint(2, "creating RenderControl")
        self._rc = RenderControl(image, self)
        dprint(2, "done")
        # selectors for extra axes
        self._wslicers = []
        curslice = self._rc.currentSlice();  # this may be loaded from config, so not necessarily 0
        for iextra, axisname, labels in self._rc.slicedAxes():
            if axisname.upper() not in ["STOKES", "COMPLEX"]:
                lbl = QLabel("%s:" % axisname, self)
                lo.addWidget(lbl)
            else:
                lbl = None
            slicer = QComboBox(self)
            self._wslicers.append(slicer)
            lo.addWidget(slicer)
            slicer.addItems(labels)
            slicer.setToolTip("""<P>Selects current slice along the %s axis.</P>""" % axisname)
            slicer.setCurrentIndex(curslice[iextra])
            QObject.connect(slicer, SIGNAL("activated(int)"), self._currier.curry(self._rc.changeSlice, iextra))
        # min/max display ranges
        lo.addSpacing(5)
        self._wrangelbl = QLabel(self)
        lo.addWidget(self._wrangelbl)
        self._minmaxvalidator = FloatValidator(self)
        self._wmin = QLineEdit(self)
        self._wmax = QLineEdit(self)
        width = self._wmin.fontMetrics().width("1.234567e-05")
        for w in self._wmin, self._wmax:
            lo.addWidget(w, 0)
            w.setValidator(self._minmaxvalidator)
            w.setMaximumWidth(width)
            w.setMinimumWidth(width)
            QObject.connect(w, SIGNAL("editingFinished()"), self._changeDisplayRange)
        # full-range button
        self._wfullrange = QToolButton(self)
        lo.addWidget(self._wfullrange, 0)
        self._wfullrange.setIcon(pixmaps.zoom_range.icon())
        self._wfullrange.setAutoRaise(True)
        QObject.connect(self._wfullrange, SIGNAL("clicked()"), self.renderControl().resetSubsetDisplayRange)
        rangemenu = QMenu(self)
        rangemenu.addAction(pixmaps.full_range.icon(), "Full subset", self.renderControl().resetSubsetDisplayRange)
        for percent in (99.99, 99.9, 99.5, 99, 98, 95):
            rangemenu.addAction("%g%%" % percent, self._currier.curry(self._changeDisplayRangeToPercent, percent))
        self._wfullrange.setPopupMode(QToolButton.DelayedPopup)
        self._wfullrange.setMenu(rangemenu)
        # update widgets from current display range
        self._updateDisplayRange(*self._rc.displayRange())
        # lock button
        self._wlock = QToolButton(self)
        self._wlock.setIcon(pixmaps.unlocked.icon())
        self._wlock.setAutoRaise(True)
        self._wlock.setToolTip("""<P>Click to lock or unlock the intensity range. When the intensity range is locked across multiple images, any changes in the intensity
          range of one are propagated to the others. Hold the button down briefly for additional options.</P>""")
        lo.addWidget(self._wlock)
        QObject.connect(self._wlock, SIGNAL("clicked()"), self._toggleDisplayRangeLock)
        QObject.connect(self.renderControl(), SIGNAL("displayRangeLocked"), self._setDisplayRangeLock)
        QObject.connect(self.renderControl(), SIGNAL("dataSubsetChanged"), self._dataSubsetChanged)
        lockmenu = QMenu(self)
        lockmenu.addAction(pixmaps.locked.icon(), "Lock all to this",
                           self._currier.curry(imgman.lockAllDisplayRanges, self.renderControl()))
        lockmenu.addAction(pixmaps.unlocked.icon(), "Unlock all", imgman.unlockAllDisplayRanges)
        self._wlock.setPopupMode(QToolButton.DelayedPopup)
        self._wlock.setMenu(lockmenu)
        self._setDisplayRangeLock(self.renderControl().isDisplayRangeLocked())
        # dialog button
        self._wshowdialog = QToolButton(self)
        lo.addWidget(self._wshowdialog)
        self._wshowdialog.setIcon(pixmaps.colours.icon())
        self._wshowdialog.setAutoRaise(True)
        self._wshowdialog.setToolTip("""<P>Click for colourmap and intensity policy options.</P>""")
        QObject.connect(self._wshowdialog, SIGNAL("clicked()"), self.showRenderControls)
        tooltip = """<P>You can change the currently displayed intensity range by entering low and high limits here.</P>
    <TABLE>
      <TR><TD><NOBR>Image min:</NOBR></TD><TD>%g</TD><TD>max:</TD><TD>%g</TD></TR>
      </TABLE>""" % self.image.imageMinMax()
        for w in self._wmin, self._wmax, self._wrangelbl:
            w.setToolTip(tooltip)

        # create image operations menu
        self._menu = QMenu(self.name, self)
        self._qa_raise = self._menu.addAction(pixmaps.raise_up.icon(), "Raise image",
                                              self._currier.curry(self.image.emit, SIGNAL("raise")))
        self._qa_center = self._menu.addAction(pixmaps.center_image.icon(), "Center plot on image",
                                               self._currier.curry(self.image.emit, SIGNAL("center")))
        self._qa_show_rc = self._menu.addAction(pixmaps.colours.icon(), "Colours && Intensities...",
                                                self.showRenderControls)
        if save:
            self._qa_save = self._menu.addAction("Save image...", self._saveImage)
        self._menu.addAction("Export image to PNG file...", self._exportImageToPNG)
        self._export_png_dialog = None
        self._menu.addAction("Unload image", self._currier.curry(self.image.emit, SIGNAL("unload")))
        self._wraise.setMenu(self._menu)
        self._wraise.setPopupMode(QToolButton.DelayedPopup)

        # connect updates from renderControl and image
        self.image.connect(SIGNAL("slice"), self._updateImageSlice)
        QObject.connect(self._rc, SIGNAL("displayRangeChanged"), self._updateDisplayRange)

        # default plot depth of image markers
        self._z_markers = None
        # and the markers themselves
        self._image_border = QwtPlotCurve()
        self._image_label = QwtPlotMarker()

        # subset markers
        self._subset_pen = QPen(QColor("Light Blue"))
        self._subset_border = QwtPlotCurve()
        self._subset_border.setPen(self._subset_pen)
        self._subset_border.setVisible(False)
        self._subset_label = QwtPlotMarker()
        text = QwtText("subset")
        text.setColor(self._subset_pen.color())
        self._subset_label.setLabel(text)
        self._subset_label.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom)
        self._subset_label.setVisible(False)
        self._setting_lmrect = False

        self._all_markers = [self._image_border, self._image_label, self._subset_border, self._subset_label]

    def close(self):
        if self._control_dialog:
            self._control_dialog.close()
            self._control_dialog = None

    def __del__(self):
        self.close()

    def __eq__(self, other):
        return self is other

    def renderControl(self):
        return self._rc

    def getMenu(self):
        return self._menu

    def getFilename(self):
        return self.image.filename

    def setName(self, name):
        self.name = name
        self._wlabel.setText("%s: %s" % (chr(ord('a') + self._number), self.name))

    def setNumber(self, num):
        self._number = num
        self._menu.menuAction().setText("%s: %s" % (chr(ord('a') + self._number), self.name))
        self._qa_raise.setShortcut(QKeySequence("Alt+" + chr(ord('A') + num)))
        self.setName(self.name)

    def getNumber(self):
        return self._number

    def setPlotProjection(self, proj):
        self.image.setPlotProjection(proj)
        sameproj = proj == self.image.projection
        self._wcenter.setVisible(sameproj)
        self._qa_center.setVisible(not sameproj)
        if self._image_border:
            (l0, l1), (m0, m1) = self.image.getExtents()
            path = numpy.array([l0, l0, l1, l1, l0]), numpy.array([m0, m1, m1, m0, m0])
            self._image_border.setData(*path)
            if self._image_label:
                self._image_label.setValue(path[0][2], path[1][2])

    def addPlotBorder(self, border_pen, label, label_color=None, bg_brush=None):
        # make plot items for image frame
        # make curve for image borders
        (l0, l1), (m0, m1) = self.image.getExtents()
        self._border_pen = QPen(border_pen)
        self._image_border.show()
        self._image_border.setData([l0, l0, l1, l1, l0], [m0, m1, m1, m0, m0])
        self._image_border.setPen(self._border_pen)
        self._image_border.setZ(self.image.z() + 1 if self._z_markers is None else self._z_markers)
        if label:
            self._image_label.show()
            self._image_label_text = text = QwtText(" %s " % label)
            text.setColor(label_color)
            text.setBackgroundBrush(bg_brush)
            self._image_label.setValue(l1, m1)
            self._image_label.setLabel(text)
            self._image_label.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
            self._image_label.setZ(self.image.z() + 2 if self._z_markers is None else self._z_markers)

    def setPlotBorderStyle(self, border_color=None, label_color=None):
        if border_color:
            self._border_pen.setColor(border_color)
            self._image_border.setPen(self._border_pen)
        if label_color:
            self._image_label_text.setColor(label_color)
            self._image_label.setLabel(self._image_label_text)

    def showPlotBorder(self, show=True):
        self._image_border.setVisible(show)
        self._image_label.setVisible(show)

    def attachToPlot(self, plot, z_markers=None):
        for item in [self.image] + self._all_markers:
            if item.plot() != plot:
                item.attach(plot)

    def setImageVisible(self, visible):
        self.image.setVisible(visible)

    def showRenderControls(self):
        if not self._control_dialog:
            dprint(1, "creating control dialog")
            self._control_dialog = ImageControlDialog(self, self._rc, self._imgman)
            dprint(1, "done")
        if not self._control_dialog.isVisible():
            dprint(1, "showing control dialog")
            self._control_dialog.show()
        else:
            self._control_dialog.hide()

    def _changeDisplayRangeToPercent(self, percent):
        if not self._control_dialog:
            self._control_dialog = ImageControlDialog(self, self._rc, self._imgman)
        self._control_dialog._changeDisplayRangeToPercent(percent)

    def _updateDisplayRange(self, dmin, dmax):
        """Updates display range widgets."""
        self._wmin.setText("%.4g" % dmin)
        self._wmax.setText("%.4g" % dmax)
        self._updateFullRangeIcon()

    def _changeDisplayRange(self):
        """Gets display range from widgets and updates the image with it."""
        try:
            newrange = float(str(self._wmin.text())), float(str(self._wmax.text()))
        except ValueError:
            return
        self._rc.setDisplayRange(*newrange)

    def _dataSubsetChanged(self, subset, minmax, desc, subset_type):
        """Called when the data subset changes (or is reset)"""
        # hide the subset indicator -- unless we're invoked while we're actually setting the subset itself
        if not self._setting_lmrect:
            self._subset = None
            self._subset_border.setVisible(False)
            self._subset_label.setVisible(False)

    def setLMRectSubset(self, rect):
        self._subset = rect
        l0, m0, l1, m1 = rect.getCoords()
        self._subset_border.setData([l0, l0, l1, l1, l0], [m0, m1, m1, m0, m0])
        self._subset_border.setVisible(True)
        self._subset_label.setValue(max(l0, l1), max(m0, m1))
        self._subset_label.setVisible(True)
        self._setting_lmrect = True
        self.renderControl().setLMRectSubset(rect)
        self._setting_lmrect = False

    def currentSlice(self):
        return self._rc.currentSlice()

    def _updateImageSlice(self, slice):
        dprint(2, slice)
        for i, (iextra, name, labels) in enumerate(self._rc.slicedAxes()):
            slicer = self._wslicers[i]
            if slicer.currentIndex() != slice[iextra]:
                dprint(3, "setting widget", i, "to", slice[iextra])
                slicer.setCurrentIndex(slice[iextra])

    def setMarkersZ(self, z):
        self._z_markers = z
        for i, elem in enumerate(self._all_markers):
            elem.setZ(z + i)

    def setZ(self, z, top=False, depthlabel=None, can_raise=True):
        self.image.setZ(z)
        if self._z_markers is None:
            for i, elem in enumerate(self._all_markers):
                elem.setZ(z + i + i)
        # set the depth label, if any
        label = "%s: %s" % (chr(ord('a') + self._number), self.name)
        # label = "%s %s"%(depthlabel,self.name) if depthlabel else self.name
        if top:
            label = "%s: <B>%s</B>" % (chr(ord('a') + self._number), self.name)
        self._wlabel.setText(label)
        # set hotkey
        self._qa_show_rc.setShortcut(Qt.Key_F9 if top else QKeySequence())
        # set raise control
        self._can_raise = can_raise
        self._qa_raise.setVisible(can_raise)
        self._wlock.setVisible(can_raise)
        if can_raise:
            self._wraise.setToolTip(
                "<P>Click here to raise this image to the top. Click on the down-arrow to access the image menu.</P>")
        else:
            self._wraise.setToolTip("<P>Click to access the image menu.</P>")

    def _raiseButtonPressed(self):
        if self._can_raise:
            self.image.emit(SIGNAL("raise"))
        else:
            self._wraise.showMenu()

    def _saveImage(self):
        filename = QFileDialog.getSaveFileName(self, "Save FITS file", self._save_dir,
                                               "FITS files(*.fits *.FITS *fts *FTS)")
        filename = str(filename)
        if not filename:
            return
        busy = BusyIndicator()
        self._imgman.showMessage("""Writing FITS image %s""" % filename, 3000)
        QApplication.flush()
        try:
            self.image.save(filename)
        except Exception as exc:
            busy = None
            traceback.print_exc()
            self._imgman.showErrorMessage("""Error writing FITS image %s: %s""" % (filename, str(sys.exc_info()[1])))
            return None
        self.renderControl().startSavingConfig(filename)
        self.setName(self.image.name)
        self._qa_save.setVisible(False)
        self._wsave.hide()
        busy = None

    def _exportImageToPNG(self, filename=None):
        if not filename:
            if not self._export_png_dialog:
                dialog = self._export_png_dialog = QFileDialog(self, "Export image to PNG", ".", "*.png")
                dialog.setDefaultSuffix("png")
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setModal(True)
                QObject.connect(dialog, SIGNAL("filesSelected(const QStringList &)"), self._exportImageToPNG)
            return self._export_png_dialog.exec_() == QDialog.Accepted
        busy = BusyIndicator()
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        # make QPixmap
        nx, ny = self.image.imageDims()
        (l0, l1), (m0, m1) = self.image.getExtents()
        pixmap = QPixmap(nx, ny)
        painter = QPainter(pixmap)
        # use QwtPlot implementation of draw canvas, since we want to avoid caching
        xmap = QwtScaleMap()
        xmap.setPaintInterval(0, nx)
        xmap.setScaleInterval(l1, l0)
        ymap = QwtScaleMap()
        ymap.setPaintInterval(ny, 0)
        ymap.setScaleInterval(m0, m1)
        self.image.draw(painter, xmap, ymap, pixmap.rect())
        painter.end()
        # save to file
        try:
            pixmap.save(filename, "PNG")
        except Exception as exc:
            self.emit(SIGNAL("showErrorMessage"), "Error writing %s: %s" % (filename, str(exc)))
            return
        self.emit(SIGNAL("showMessage"), "Exported image to file %s" % filename)

    def _toggleDisplayRangeLock(self):
        self.renderControl().lockDisplayRange(not self.renderControl().isDisplayRangeLocked())

    def _setDisplayRangeLock(self, locked):
        self._wlock.setIcon(pixmaps.locked.icon() if locked else pixmaps.unlocked.icon())

    def _updateFullRangeIcon(self):
        if self._rc.isSubsetDisplayRange():
            self._wfullrange.setIcon(pixmaps.zoom_range.icon())
            self._wfullrange.setToolTip(
                """<P>The current intensity range is the full range. Hold this button down briefly for additional options.</P>""")
        else:
            self._wfullrange.setIcon(pixmaps.full_range.icon())
            self._wfullrange.setToolTip(
                """<P>Click to reset to a full intensity range. Hold the button down briefly for additional options.</P>""")
Exemple #59
0
    def show_context_menu(self, point):
        item = self.itemAt(point)
        if item is None or item in set(self.categories.itervalues()):
            return
        m = QMenu(self)
        sel = self.selectedItems()
        num = len(sel)
        container = current_container()
        ci = self.currentItem()
        if ci is not None:
            cn = unicode(ci.data(0, NAME_ROLE).toString())
            mt = unicode(ci.data(0, MIME_ROLE).toString())
            cat = unicode(ci.data(0, CATEGORY_ROLE).toString())
            n = elided_text(cn.rpartition('/')[-1])
            m.addAction(QIcon(I('save.png')),
                        _('Export %s') % n, partial(self.export, cn))
            if cn not in container.names_that_must_not_be_changed and cn not in container.names_that_must_not_be_removed and mt not in OEB_FONTS:
                m.addAction(
                    _('Replace %s with file...') % n,
                    partial(self.replace, cn))
            m.addSeparator()

            m.addAction(QIcon(I('modified.png')),
                        _('&Rename %s') % n, self.edit_current_item)
            if is_raster_image(mt):
                m.addAction(QIcon(I('default_cover.png')),
                            _('Mark %s as cover image') % n,
                            partial(self.mark_as_cover, cn))
            elif current_container(
            ).SUPPORTS_TITLEPAGES and mt in OEB_DOCS and cat == 'text':
                m.addAction(QIcon(I('default_cover.png')),
                            _('Mark %s as cover page') % n,
                            partial(self.mark_as_titlepage, cn))
            m.addSeparator()

        if num > 0:
            m.addSeparator()
            if num > 1:
                m.addAction(QIcon(I('modified.png')),
                            _('&Bulk rename selected files'),
                            self.request_bulk_rename)
            m.addAction(QIcon(I('trash.png')),
                        _('&Delete the %d selected file(s)') % num,
                        self.request_delete)
            m.addSeparator()

        selected_map = defaultdict(list)
        for item in sel:
            selected_map[unicode(item.data(
                0, CATEGORY_ROLE).toString())].append(
                    unicode(item.data(0, NAME_ROLE).toString()))

        for items in selected_map.itervalues():
            items.sort(key=self.index_of_name)

        if selected_map['text']:
            m.addAction(QIcon(I('format-text-color.png')),
                        _('Link &stylesheets...'),
                        partial(self.link_stylesheets, selected_map['text']))

        if len(selected_map['text']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected text files'),
                partial(self.start_merge, 'text', selected_map['text']))
        if len(selected_map['styles']) > 1:
            m.addAction(
                QIcon(I('merge.png')), _('&Merge selected style files'),
                partial(self.start_merge, 'styles', selected_map['styles']))

        if len(list(m.actions())) > 0:
            m.popup(self.mapToGlobal(point))
Exemple #60
0
class EditorWidget(QWebView):  # {{{

    def __init__(self, parent=None):
        QWebView.__init__(self, parent)
        self.readonly = False

        self.comments_pat = re.compile(r'<!--.*?-->', re.DOTALL)

        extra_shortcuts = {
                'ToggleBold': 'Bold',
                'ToggleItalic': 'Italic',
                'ToggleUnderline': 'Underline',
        }

        for wac, name, icon, text, checkable in [
                ('ToggleBold', 'bold', 'format-text-bold', _('Bold'), True),
                ('ToggleItalic', 'italic', 'format-text-italic', _('Italic'),
                    True),
                ('ToggleUnderline', 'underline', 'format-text-underline',
                    _('Underline'), True),
                ('ToggleStrikethrough', 'strikethrough', 'format-text-strikethrough',
                    _('Strikethrough'), True),
                ('ToggleSuperscript', 'superscript', 'format-text-superscript',
                    _('Superscript'), True),
                ('ToggleSubscript', 'subscript', 'format-text-subscript',
                    _('Subscript'), True),
                ('InsertOrderedList', 'ordered_list', 'format-list-ordered',
                    _('Ordered list'), True),
                ('InsertUnorderedList', 'unordered_list', 'format-list-unordered',
                    _('Unordered list'), True),

                ('AlignLeft', 'align_left', 'format-justify-left',
                    _('Align left'), False),
                ('AlignCenter', 'align_center', 'format-justify-center',
                    _('Align center'), False),
                ('AlignRight', 'align_right', 'format-justify-right',
                    _('Align right'), False),
                ('AlignJustified', 'align_justified', 'format-justify-fill',
                    _('Align justified'), False),
                ('Undo', 'undo', 'edit-undo', _('Undo'), False),
                ('Redo', 'redo', 'edit-redo', _('Redo'), False),
                ('RemoveFormat', 'remove_format', 'trash', _('Remove formatting'), False),
                ('Copy', 'copy', 'edit-copy', _('Copy'), False),
                ('Paste', 'paste', 'edit-paste', _('Paste'), False),
                ('Cut', 'cut', 'edit-cut', _('Cut'), False),
                ('Indent', 'indent', 'format-indent-more',
                    _('Increase Indentation'), False),
                ('Outdent', 'outdent', 'format-indent-less',
                    _('Decrease Indentation'), False),
                ('SelectAll', 'select_all', 'edit-select-all',
                    _('Select all'), False),
            ]:
            ac = PageAction(wac, icon, text, checkable, self)
            setattr(self, 'action_'+name, ac)
            ss = extra_shortcuts.get(wac, None)
            if ss:
                ac.setShortcut(QKeySequence(getattr(QKeySequence, ss)))
            if wac == 'RemoveFormat':
                ac.triggered.connect(self.remove_format_cleanup,
                        type=Qt.QueuedConnection)

        self.action_color = QAction(QIcon(I('format-text-color')), _('Foreground color'),
                self)
        self.action_color.triggered.connect(self.foreground_color)

        self.action_background = QAction(QIcon(I('format-fill-color')),
                _('Background color'), self)
        self.action_background.triggered.connect(self.background_color)

        self.action_block_style = QAction(QIcon(I('format-text-heading')),
                _('Style text block'), self)
        self.action_block_style.setToolTip(
                _('Style the selected text block'))
        self.block_style_menu = QMenu(self)
        self.action_block_style.setMenu(self.block_style_menu)
        self.block_style_actions = []
        for text, name in [
                (_('Normal'), 'p'),
                (_('Heading') +' 1', 'h1'),
                (_('Heading') +' 2', 'h2'),
                (_('Heading') +' 3', 'h3'),
                (_('Heading') +' 4', 'h4'),
                (_('Heading') +' 5', 'h5'),
                (_('Heading') +' 6', 'h6'),
                (_('Pre-formatted'), 'pre'),
                (_('Blockquote'), 'blockquote'),
                (_('Address'), 'address'),
                ]:
            ac = BlockStyleAction(text, name, self)
            self.block_style_menu.addAction(ac)
            self.block_style_actions.append(ac)

        self.action_insert_link = QAction(QIcon(I('insert-link.png')),
                _('Insert link or image'), self)
        self.action_insert_link.triggered.connect(self.insert_link)
        self.pageAction(QWebPage.ToggleBold).changed.connect(self.update_link_action)
        self.action_insert_link.setEnabled(False)
        self.action_clear = QAction(QIcon(I('edit-clear')), _('Clear'), self)
        self.action_clear.triggered.connect(self.clear_text)

        self.page().setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
        self.page().linkClicked.connect(self.link_clicked)

        self.setHtml('')
        self.set_readonly(False)

    def update_link_action(self):
        wac = self.pageAction(QWebPage.ToggleBold)
        self.action_insert_link.setEnabled(wac.isEnabled())

    def set_readonly(self, what):
        self.readonly = what
        self.page().setContentEditable(not self.readonly)

    def clear_text(self, *args):
        us = self.page().undoStack()
        us.beginMacro('clear all text')
        self.action_select_all.trigger()
        self.action_remove_format.trigger()
        self.exec_command('delete')
        us.endMacro()
        self.set_font_style()
        self.setFocus(Qt.OtherFocusReason)

    def link_clicked(self, url):
        open_url(url)

    def foreground_color(self):
        col = QColorDialog.getColor(Qt.black, self,
                _('Choose foreground color'), QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('foreColor', unicode(col.name()))

    def background_color(self):
        col = QColorDialog.getColor(Qt.white, self,
                _('Choose background color'), QColorDialog.ShowAlphaChannel)
        if col.isValid():
            self.exec_command('hiliteColor', unicode(col.name()))

    def insert_link(self, *args):
        link, name, is_image = self.ask_link()
        if not link:
            return
        url = self.parse_link(link)
        if url.isValid():
            url = unicode(url.toString())
            self.setFocus(Qt.OtherFocusReason)
            if is_image:
                self.exec_command('insertHTML',
                        '<img src="%s" alt="%s"></img>'%(prepare_string_for_xml(url, True),
                            prepare_string_for_xml(name or _('Image'), True)))
            elif name:
                self.exec_command('insertHTML',
                        '<a href="%s">%s</a>'%(prepare_string_for_xml(url, True),
                            prepare_string_for_xml(name)))
            else:
                self.exec_command('createLink', url)
        else:
            error_dialog(self, _('Invalid URL'),
                         _('The url %r is invalid') % link, show=True)

    def ask_link(self):
        d = QDialog(self)
        d.setWindowTitle(_('Create link'))
        l = QFormLayout()
        d.setLayout(l)
        d.url = QLineEdit(d)
        d.name = QLineEdit(d)
        d.treat_as_image = QCheckBox(d)
        d.setMinimumWidth(600)
        d.bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
        d.br = b = QPushButton(_('&Browse'))
        b.setIcon(QIcon(I('document_open.png')))
        def cf():
            files = choose_files(d, 'select link file', _('Choose file'), select_only_single_file=True)
            if files:
                path = files[0]
                d.url.setText(path)
                if path and os.path.exists(path):
                    with lopen(path, 'rb') as f:
                        q = what(f)
                    is_image = q in {'jpeg', 'png', 'gif'}
                    d.treat_as_image.setChecked(is_image)

        b.clicked.connect(cf)
        d.la = la = QLabel(_(
            'Enter a URL. If you check the "Treat the URL as an image" box '
            'then the URL will be added as an image reference instead of as '
            'a link. You can also choose to create a link to a file on '
            'your computer. '
            'Note that if you create a link to a file on your computer, it '
            'will stop working if the file is moved.'))
        la.setWordWrap(True)
        la.setStyleSheet('QLabel { margin-bottom: 1.5ex }')
        l.setWidget(0, l.SpanningRole, la)
        l.addRow(_('Enter &URL:'), d.url)
        l.addRow(_('Treat the URL as an &image'), d.treat_as_image)
        l.addRow(_('Enter &name (optional):'), d.name)
        l.addRow(_('Choose a file on your computer:'), d.br)
        l.addRow(d.bb)
        d.bb.accepted.connect(d.accept)
        d.bb.rejected.connect(d.reject)
        d.resize(d.sizeHint())
        link, name, is_image = None, None, False
        if d.exec_() == d.Accepted:
            link, name = unicode(d.url.text()).strip(), unicode(d.name.text()).strip()
            is_image = d.treat_as_image.isChecked()
        return link, name, is_image

    def parse_link(self, link):
        link = link.strip()
        if link and os.path.exists(link):
            return QUrl.fromLocalFile(link)
        has_schema = re.match(r'^[a-zA-Z]+:', link)
        if has_schema is not None:
            url = QUrl(link, QUrl.TolerantMode)
            if url.isValid():
                return url
        if os.path.exists(link):
            return QUrl.fromLocalFile(link)

        if has_schema is None:
            first, _, rest = link.partition('.')
            prefix = 'http'
            if first == 'ftp':
                prefix = 'ftp'
            url = QUrl(prefix +'://'+link, QUrl.TolerantMode)
            if url.isValid():
                return url

        return QUrl(link, QUrl.TolerantMode)

    def sizeHint(self):
        return QSize(150, 150)

    def exec_command(self, cmd, arg=None):
        frame = self.page().mainFrame()
        if arg is not None:
            js = 'document.execCommand("%s", false, %s);' % (cmd,
                    json.dumps(unicode(arg)))
        else:
            js = 'document.execCommand("%s", false, null);' % cmd
        frame.evaluateJavaScript(js)

    def remove_format_cleanup(self):
        self.html = self.html

    @dynamic_property
    def html(self):

        def fget(self):
            ans = u''
            try:
                check = unicode(self.page().mainFrame().toPlainText()).strip()
                raw = unicode(self.page().mainFrame().toHtml())
                raw = xml_to_unicode(raw, strip_encoding_pats=True,
                                    resolve_entities=True)[0]
                raw = self.comments_pat.sub('', raw)
                if not check and '<img' not in raw.lower():
                    return ans

                try:
                    root = html.fromstring(raw)
                except:
                    root = fromstring(raw)

                elems = []
                for body in root.xpath('//body'):
                    if body.text:
                        elems.append(body.text)
                    elems += [html.tostring(x, encoding=unicode) for x in body if
                        x.tag not in ('script', 'style')]

                if len(elems) > 1:
                    ans = u'<div>%s</div>'%(u''.join(elems))
                else:
                    ans = u''.join(elems)
                    if not ans.startswith('<'):
                        ans = '<p>%s</p>'%ans
                ans = xml_replace_entities(ans)
            except:
                import traceback
                traceback.print_exc()

            return ans

        def fset(self, val):
            self.setHtml(val)
            self.set_font_style()
        return property(fget=fget, fset=fset)

    def set_font_style(self):
        fi = QFontInfo(QApplication.font(self))
        f  = fi.pixelSize() + 1 + int(tweaks['change_book_details_font_size_by'])
        fam = unicode(fi.family()).strip().replace('"', '')
        if not fam:
            fam = 'sans-serif'
        style = 'font-size: %fpx; font-family:"%s",sans-serif;' % (f, fam)

        # toList() is needed because PyQt on Debian is old/broken
        for body in self.page().mainFrame().documentElement().findAll('body').toList():
            body.setAttribute('style', style)
        self.page().setContentEditable(not self.readonly)

    def keyPressEvent(self, ev):
        if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
            ev.ignore()
        else:
            return QWebView.keyPressEvent(self, ev)

    def keyReleaseEvent(self, ev):
        if ev.key() in (Qt.Key_Tab, Qt.Key_Escape, Qt.Key_Backtab):
            ev.ignore()
        else:
            return QWebView.keyReleaseEvent(self, ev)

    def contextMenuEvent(self, ev):
        menu = self.page().createStandardContextMenu()
        paste = self.pageAction(QWebPage.Paste)
        for action in menu.actions():
            if action == paste:
                menu.insertAction(action, self.pageAction(QWebPage.PasteAndMatchStyle))
        menu.exec_(ev.globalPos())