コード例 #1
0
ファイル: choose_library.py プロジェクト: zhongsifen/calibre
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.prev_lname = self.last_lname = ''
        self.count_changed(0)
        self.action_choose = self.menuless_qaction
        self.action_exim = ac = QAction(_('Export/import all calibre data'),
                                        self.gui)
        ac.triggered.connect(self.exim_data)

        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)
            self.choose_menu.addAction(self.action_exim)
        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)
            ac.setObjectName(unicode_type(i))
            self.switch_actions.append(ac)
            ac.setVisible(False)
            connect_lambda(
                ac.triggered,
                self,
                lambda self: self.switch_requested(self.qs_locations[int(
                    self.gui.sender().objectName())]),
                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)
        ac = self.create_action(spec=(_('Switch to previous library'),
                                      'lt.png', None, None),
                                attr='action_previous_library')
        ac.triggered.connect(self.switch_to_previous_library,
                             type=Qt.QueuedConnection)
        self.gui.keyboard.register_shortcut(self.unique_name + '-' +
                                            'action_previous_library',
                                            ac.text(),
                                            action=ac,
                                            group=self.action_spec[0],
                                            default_keys=('Ctrl+Alt+p', ))
        self.gui.addAction(ac)

    @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 exim_data(self):
        if isportable:
            return error_dialog(
                self.gui,
                _('Cannot export/import'),
                _('You are running calibre portable, all calibre data is already in the'
                  ' calibre portable folder. Export/import is unavailable.'),
                show=True)
        if self.gui.job_manager.has_jobs():
            return error_dialog(
                self.gui,
                _('Cannot export/import'),
                _('Cannot export/import data while there are running jobs.'),
                show=True)
        from calibre.gui2.dialogs.exim import EximDialog
        d = EximDialog(parent=self.gui)
        if d.exec_() == d.Accepted:
            if d.restart_needed:
                self.gui.iactions['Restart'].restart()

    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' + ngettext(
            '{0} [{1} book]', '{0} [{1} books]', count).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)
        if lname != self.last_lname:
            self.prev_lname = self.last_lname
            self.last_lname = lname
        if len(lname) > 16:
            lname = lname[:16] + '…'
        a = self.qaction
        a.setText(lname.replace(
            '&', '&&&'))  # I have no idea why this requires a triple ampersand
        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 switch_to_previous_library(self):
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))
        for name, loc in locations:
            is_prev_lib = name == self.prev_lname
            if is_prev_lib:
                self.switch_requested(loc)
                break

    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.rename_menu.clear()
        self.delete_menu.clear()
        quick_actions, rename_actions, delete_actions = [], [], []
        for name, loc in locations:
            is_prev_lib = name == self.prev_lname
            name = name.replace('&', '&&')
            ac = self.quick_menu.addAction(
                name, Dispatcher(partial(self.switch_requested, loc)))
            ac.setStatusTip(_('Switch to: %s') % loc)
            if is_prev_lib:
                f = ac.font()
                f.setBold(True)
                ac.setFont(f)
            quick_actions.append(ac)
            ac = self.rename_menu.addAction(
                name, Dispatcher(partial(self.rename_requested, name, loc)))
            rename_actions.append(ac)
            ac.setStatusTip(_('Rename: %s') % loc)
            ac = self.delete_menu.addAction(
                name, Dispatcher(partial(self.delete_requested, name, loc)))
            delete_actions.append(ac)
            ac.setStatusTip(_('Remove: %s') % loc)
            if is_prev_lib:
                ac.setFont(f)

        qs_actions = []
        locations_by_frequency = locations
        if len(locations) >= tweaks['many_libraries']:
            locations_by_frequency = list(
                self.stats.locations(db, limit=sys.maxsize))
        for i, x in enumerate(
                locations_by_frequency[:len(self.switch_actions)]):
            name, loc = x
            name = name.replace('&', '&&')
            ac = self.switch_actions[i]
            ac.setText(name)
            ac.setStatusTip(_('Switch to: %s') % loc)
            ac.setVisible(True)
            qs_actions.append(ac)
        self.qs_locations = [i[1] for i in locations_by_frequency]

        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)
        # Allow the cloned actions in the OS X global menubar to update
        for a in (self.qaction, self.menuless_qaction):
            a.changed.emit()

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

    def rename_requested(self, name, location):
        LibraryDatabase = db_class()
        loc = location.replace('/', os.sep)
        base = os.path.dirname(loc)
        old_name = name.replace('&&', '&')
        newname, ok = QInputDialog.getText(
            self.gui,
            _('Rename') + ' ' + old_name,
            '<p>' + _('Choose a new name for the library <b>%s</b>. ') % name +
            '<p>' + _('Note that the actual library folder will be renamed.'),
            text=old_name)
        newname = sanitize_file_name(unicode_type(newname))
        if not ok or not newname or newname == old_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
        self.gui.library_broker.remove_library(loc)
        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)
        if not question_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.') %
            ('<code>%s</code>' % loc),
                override_icon='dialog_information.png',
                yes_text=_('&OK'),
                no_text=_('&Undo'),
                yes_icon='ok.png',
                no_icon='edit-undo.png'):
            return
        self.stats.remove(location)
        self.gui.library_broker.remove_library(location)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()
        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)

    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
        library_path = db.library_path

        d = DBCheck(self.gui, db)
        d.start()
        try:
            m.close()
        except:
            pass
        d.break_cycles()
        self.gui.library_moved(library_path)
        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 PyQt5.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 range(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 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_()

    def choose_library_callback(self,
                                newloc,
                                copy_structure=False,
                                library_renamed=False):
        self.gui.library_moved(newloc,
                               copy_structure=copy_structure,
                               allow_rebuild=True)
        if library_renamed:
            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

        if self.gui.proceed_question.questions:
            warning_dialog(self.gui,
                           _('Not allowed'),
                           _('You cannot change libraries until all'
                             ' updates are accepted or rejected.'),
                           show=True)
            return False

        return True
コード例 #2
0
ファイル: choose_library.py プロジェクト: AtulKumar2/calibre
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)))
            ac.setStatusTip(_('Switch to: %s') % loc)
            quick_actions.append(ac)
            ac = self.rename_menu.addAction(name, Dispatcher(partial(self.rename_requested,
                name, loc)))
            rename_actions.append(ac)
            ac.setStatusTip(_('Rename: %s') % loc)
            ac = self.delete_menu.addAction(name, Dispatcher(partial(self.delete_requested,
                name, loc)))
            delete_actions.append(ac)
            ac.setStatusTip(_('Remove: %s') % loc)

        qs_actions = []
        for i, x in enumerate(locations[:len(self.switch_actions)]):
            name, loc = x
            ac = self.switch_actions[i]
            ac.setText(name)
            ac.setStatusTip(_('Switch to: %s') % loc)
            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)
        # Allow the cloned actions in the OS X global menubar to update
        for a in (self.qaction, self.menuless_qaction):
            a.changed.emit()

    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)
        if not question_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.') % ('<code>%s</code>' % loc),
                override_icon='dialog_information.png',
                yes_text=_('&OK'), no_text=_('&Undo'), yes_icon='ok.png', no_icon='edit-undo.png'):
            return
        self.stats.remove(location)
        self.build_menus()
        self.gui.iactions['Copy To Library'].build_menus()
        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 PyQt5.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
コード例 #3
0
ファイル: tag_mapper.py プロジェクト: JapaChin/calibre
class RulesDialog(Dialog):

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

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

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

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

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

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

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

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

    def test_rules(self):
        Tester(self.rules, self).exec_()
コード例 #4
0
ファイル: ui.py プロジェクト: dickloraine/EmbedComicMetadata
class EmbedComicMetadata(InterfaceAction):

    name = 'Embed Comic Metadata'

    # Declare the main action associated with this plugin
    if prefs["main_import"]:
        action_spec = (_L['Import Comic Metadata'], None,
                _L['Imports the metadata from the comic to calibre'], None)
    else:
        action_spec = (_L['Embed Comic Metadata'], None,
                _L['Embeds calibres metadata into the comic'], None)

    def genesis(self):
        # menu
        self.menu = QMenu(self.gui)

        # Set the icon for this interface action
        icon = get_icons('images/icon.png')  # need to import this?

        # The qaction is automatically created from the action_spec defined
        # above
        self.qaction.setMenu(self.menu)
        self.qaction.setIcon(icon)
        self.qaction.triggered.connect(self.main_menu_triggered)

        # build menu
        self.menu.clear()
        self.build_menu()
        self.toggle_menu_items()

    def build_menu(self):
        for item in config[CONFIG_MENU]["UI_Action_Items"]:
            if item[CONFIG_NAME] == "seperator":
                self.menu.addSeparator()
                continue
            elif item[CONFIG_TRIGGER_ARG]:
                triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self, item[CONFIG_TRIGGER_ARG])
            else:
                triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self)
            self.menu_action(item[CONFIG_NAME], item[CONFIG_DESCRIPTION], triggerfunc)         
        # add configuration entry
        self.menu_action("configure", _L["Configure"],
                         partial(self.interface_action_base_plugin.do_user_config, (self.gui)))

    def toggle_menu_items(self):
        for item in config[CONFIG_MENU]["Items"]:
            action = getattr(self, item[CONFIG_NAME])
            action.setVisible(prefs[item[CONFIG_NAME]])

    def main_menu_triggered(self):
        from calibre_plugins.EmbedComicMetadata.main import embed_into_comic, import_to_calibre

        # Check the preferences for what should be done
        if (prefs['read_cbi'] and prefs['read_cix']) or (prefs['cbi_embed'] and prefs['cix_embed']):
            action = "both"
        elif (prefs['read_cbi']) or (prefs['cbi_embed']):
            action = "cbi"
        elif (prefs['read_cix']) or (prefs['cix_embed']):
            action = "cix"
        else:
            return error_dialog(self.gui, _L['Cannot update metadata'],
                        _L['No embed format selected'], show=True)

        if prefs["main_import"]:
            import_to_calibre(self, action)
        else:
            embed_into_comic(self, action)

    def apply_settings(self):
        from calibre_plugins.EmbedComicMetadata.config import prefs
        # In an actual non trivial plugin, you would probably need to
        # do something based on the settings in prefs
        prefs

    def menu_action(self, name, title, triggerfunc):
        action = self.create_menu_action(self.menu, name, title, icon=None,
            shortcut=None, description=None, triggered=triggerfunc,
            shortcut_name=None)
        setattr(self, name, action)
コード例 #5
0
class ExecMacroAction(InterfaceAction):

    name = 'Exec Macro'
    # Create our top-level menu/toolbar action (text, icon_path, tooltip, keyboard shortcut)
    action_spec = ('Exec Macro', None, 'Execute current selected macro.', ())
    action_type = 'current'

    def genesis(self):
        # This method is called once per plugin, do initial setup here
        self.menu = QMenu(self.gui)
        self.actions = {}
        self.rebuild_menu()

        self.qaction.setMenu(self.menu)
        self.qaction.setIcon(get_icons('images/icon.png'))
        self.qaction.triggered.connect(self.execute_current_macro)

    def rebuild_menu(self):
        self.menu.clear()
        self.actions.clear()

        macros = prefs['macros']
        for name in sorted(macros):
            action = self.create_menu_action_unique(
                self.menu,
                name,
                tooltip=macros[name]['documentation'],
                image='dot_red.png',
                shortcut=None,
                shortcut_name=None,
                triggered=partial(self.execute_macro, name))
            self.actions[name] = action
        self.mark_current_macro()

        self.menu.addSeparator()
        self.create_menu_action_unique(self.menu,
                                       _('Manage macros'),
                                       tooltip=None,
                                       image='config.png',
                                       shortcut=None,
                                       shortcut_name=None,
                                       triggered=partial(
                                           show_config_dialog, self))

        self.delete_orphan_shortcuts()

    def delete_orphan_shortcuts(self):
        prefix = menu_action_unique_name(self, '')
        to_del = {
            sc
            for sc in self.gui.keyboard.shortcuts if sc.startswith(prefix)
        }
        new_sc = {menu_action_unique_name(self, name) for name in self.actions}
        new_sc.add(menu_action_unique_name(self, _('Manage macros')))
        to_del -= new_sc
        for sc in to_del:
            self.gui.keyboard.unregister_shortcut(sc)
        self.gui.keyboard.finalize()

    def mark_current_macro(self):
        for name, action in self.actions.iteritems():
            action.setIconVisibleInMenu(name == prefs['current_macro'])

    def execute_current_macro(self):
        self.execute_macro(prefs['current_macro'])

    def execute_macro(self, name):
        if name != prefs['current_macro']:
            prefs['current_macro'] = name
            self.mark_current_macro()

        macro = prefs['macros'].get(name, None)
        if not macro:
            return

        log = GUILog()
        log.outputs.append(ANSIStream())
        try:
            if macro.get('execfromfile'):
                with open(macro['macrofile'], 'rU') as file:
                    self.execute(file, log)
            elif macro['program']:
                program = macro['program']
                encoding = self.get_encoding(program)
                if encoding:
                    program = program.encode(encoding)
                self.execute(program, log)
        except:
            log.exception(_('Failed to execute macro'))
            error_dialog(
                self.gui,
                _('Failed to execute macro'),
                _('Failed to execute macro, click "Show details" for more information.'
                  ),
                det_msg=log.plain_text,
                show=True)

    def execute(self, program, log):
        # exec(program, globals().copy(), locals().copy())
        vars = globals().copy()
        vars['self'] = self
        vars['log'] = log
        exec(program, vars)

    def create_menu_action_unique(self,
                                  parent_menu,
                                  menu_text,
                                  image=None,
                                  tooltip=None,
                                  shortcut=None,
                                  triggered=None,
                                  is_checked=None,
                                  shortcut_name=None,
                                  unique_name=None):
        '''
        Create a menu action with the specified criteria and action, using the new
        InterfaceAction.create_menu_action() function which ensures that regardless of
        whether a shortcut is specified it will appear in Preferences->Keyboard
        '''
        orig_shortcut = shortcut
        kb = self.gui.keyboard
        if unique_name is None:
            unique_name = menu_text
        if not shortcut == False:
            full_unique_name = menu_action_unique_name(self, unique_name)
            if full_unique_name in kb.shortcuts:
                shortcut = False
            else:
                if shortcut is not None and not shortcut == False:
                    if len(shortcut) == 0:
                        shortcut = None
                    else:
                        shortcut = _(shortcut)

        if shortcut_name is None:
            shortcut_name = menu_text.replace('&', '')

        ac = self.create_menu_action(parent_menu,
                                     unique_name,
                                     menu_text,
                                     icon=None,
                                     shortcut=shortcut,
                                     description=tooltip,
                                     triggered=triggered,
                                     shortcut_name=shortcut_name)
        if shortcut == False and not orig_shortcut == False:
            if ac.calibre_shortcut_unique_name in self.gui.keyboard.shortcuts:
                kb.replace_action(ac.calibre_shortcut_unique_name, ac)
        if image:
            ac.setIcon(QIcon(I(image)))
        if is_checked is not None:
            ac.setCheckable(True)
            if is_checked:
                ac.setChecked(True)

        return ac

    def get_encoding(self, txt):
        decl_re = re.compile(r'^[ \t\f]*#.*coding[:=][ \t]*([-\w.]+)')
        blank_re = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)')

        lines = txt.splitlines()

        if len(lines) < 1:
            return None
        match = decl_re.match(lines[0])
        if match:
            return match.group(1)

        if len(lines) < 2 or (not blank_re.match(lines[0])):
            return None
        match = decl_re.match(lines[1])
        if match:
            return match.group(1)

        return None
コード例 #6
0
class RulesDialog(Dialog):

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

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

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

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

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

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

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

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

    def test_rules(self):
        Tester(self.rules, self).exec_()
コード例 #7
0
ファイル: Manager.py プロジェクト: razman786/tigger
class ImageManager(QWidget):
    """An ImageManager manages a stack of images (and associated ImageControllers)"""
    showErrorMessage = pyqtSignal(str, int)
    imagesChanged = pyqtSignal()
    imageRaised = pyqtSignal(FITSImagePlotItem)
    imagePlotRaised = pyqtSignal()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def getCenterImage(self):
        return self._center_image

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

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

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

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

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

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

    def getMenu(self):
        return self._menu

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

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

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

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

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

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

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

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

    def _createImageController(self,
                               image,
                               name,
                               basename,
                               model=False,
                               save=False):
        dprint(2, "creating ImageController for", name)
        ic = ImageController(image, self, self, name, save=save)
        # attach appropriate signals
        ic.imageSignalRepaint.connect(self.replot)
        ic.imageSignalSlice.connect(self.fastReplot)
        image.connectPlotRiased(self.imagePlotRaised)
        ic.imageSignalRaise.connect(self._currier.curry(self.raiseImage, ic))
        ic.imageSignalUnload.connect(self._currier.curry(self.unloadImage, ic))
        ic.imageSignalCenter.connect(self._currier.curry(self.centerImage, ic))
        ic.setNumber(len(self._imagecons))
        self._imagecons.insert(0, ic)
        self._imagecon_loadorder.append(ic)
        if model:
            self._model_imagecons.add(id(ic))
        self._lo.addWidget(ic)
        if self._border_pen:
            ic.addPlotBorder(self._border_pen, basename, self._label_color,
                             self._label_bg_brush)
        ic.renderControl().displayRangeChanged.connect(
            self._currier.curry(self._updateDisplayRange, ic.renderControl()))
        ic.renderControl().displayRangeLocked.connect(
            self._currier.curry(self._lockDisplayRange, ic.renderControl()))
        self._plot = None
        # add to menus
        dprint(2, "repopulating menus")
        self._repopulateMenu()
        # center and raise to top of stack
        self.raiseImage(ic)
        if not self._center_image:
            self.centerImage(ic, emit=False)
        else:
            ic.setPlotProjection(self._center_image.projection)
        # signal
        self.imagesChanged.emit()
        return ic
コード例 #8
0
ファイル: book_layout.py プロジェクト: cyril711/git-MythicWar
class BookLayout (QWidget, Ui_BookLayout):
    def __init__ (self,  parent=None):
        super(BookLayout, self).__init__(parent)
        self.setupUi(self)
        self.next.clicked.connect(self.goNext)
        self.previous.clicked.connect(self.goPrevious)
        self.treeView = None
        self.pages_widget = []
        self.model = None

        
    def load (self,model=None):
        if model!=None:
            self.model = model
            self.model.print_()
        if self.treeView != None:
            self.treeView.disconnect()
            self.treeView.setParent(None)
            self.tree_view_layout.removeWidget(self.treeView)
            self.tree_model.disconnect()
            self.tree_model.setParent(None)
        else:
            self.stackedWidget.removeWidget(self.page_3)
            self.stackedWidget.removeWidget(self.page_4)
        for page_widget in self.pages_widget :
            page_widget.setParent(None)
            self.stackedWidget.removeWidget(page_widget)
        self.pages_widget .clear()
        
        
        self.treeView = CustomTreeView(self.tree_view_page)
#         self.treeView.setDragEnabled(True)
#         self.treeView.setAcceptDrops(True)
        self.treeView.setDropIndicatorShown(True)
        self.treeView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
        self.treeView.setObjectName("treeView")
        self.tree_view_layout.addWidget(self.treeView)
        self.treeView.setIndentation(10)
        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.treeView.customContextMenuRequested.connect(self.onContextMenu)
        self.treeView.activated.connect(self.changeCurrentPage)
        self.tree_model = TreeModel(self.model)
        self.tree_model.dataChanged.connect(self.updateModel)
        self.treeView.setModel(self.tree_model)
        self.treeView.setWindowTitle("Simple Tree Model")
        self.treeView.header().hide()
        self.treeView.setAlternatingRowColors(True)
        self.contextMenu = QMenu(self.treeView)


        for child in self.model.root.children:
            self.processChapter(child)

        print ('nombre de page :',self.stackedWidget.count())
        if self.stackedWidget.count() >= 1 :
            self.next.setEnabled(True)

#         self.stackedWidget.removeWidget(self.page)
#         self.stackedWidget.removeWidget(self.page_2)
#         self.stackedWidget.addWidget(self.book_homepage)
        self.stackedWidget.setCurrentIndex(0)



    def reload (self):
        print("-----------------")
        self.model.print_()
        if self.treeView != None:
            self.treeView.setParent(None)
            self.tree_view_layout.removeWidget(self.treeView)
        else:
            self.stackedWidget.removeWidget(self.page_3)
            self.stackedWidget.removeWidget(self.page_4)
        for page_widget in self.pages_widget :
            page_widget.setParent(None)
            self.stackedWidget.removeWidget(page_widget)
        self.pages_widget.clear()
        
        
        self.treeView = CustomTreeView(self.tree_view_page)
        self.treeView.setDropIndicatorShown(True)
        self.treeView.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
        self.treeView.setObjectName("treeView")
        self.tree_view_layout.addWidget(self.treeView)
        self.treeView.setIndentation(10)
        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.treeView.customContextMenuRequested.connect(self.onContextMenu)
        self.treeView.activated.connect(self.changeCurrentPage)
        self.tree_model = TreeModel(self.model)
        self.tree_model.dataChanged.connect(self.updateModel)
        self.treeView.setModel(self.tree_model)
        self.treeView.setWindowTitle("Simple Tree Model")
        self.treeView.header().hide()
        self.treeView.setAlternatingRowColors(True)
        self.contextMenu = QMenu(self.treeView)
        
#         print ('len(self.model.children())', len(self.model.children()))
 
        for child in self.model.root.children:
            self.processChapter(child)
#
        print ('nombre de page :',self.stackedWidget.count())
        if self.stackedWidget.count() >= 1 :
            self.next.setEnabled(True)

        self.stackedWidget.setCurrentIndex(0)


    def updateModel(self):
        print ('change drag and drop')
        root = self.tree_model.rootItem
        self.model = Book()
        self.model.root = Chapitre(None,root.data(0),root.data(1))
        for child in root.children:
            self.processUpdateModel(self.model.root,child)
            
    def processUpdateModel(self,parent,node):
        print ('processUpdateModel')
        chap = Chapitre(parent,node.data(0),node.data(1))
        parent.addChild(chap)
        for c in node.children:
            self.processUpdateModel(chap,c)
            
            

    def processChapter (self, chapitre):
        if len(chapitre.children) != 0:
            for sub in chapitre.children :
                self.processChapter(sub)
            
        else:
            if chapitre.content != None :
                basepath = Config().instance.path_to_book()
                filename= os.path.join(basepath,chapitre.content)
                w_page = PageWidget (filename, self)
                self.stackedWidget.addWidget(w_page)
                self.pages_widget.append(w_page)
                chapitre.indice = self.stackedWidget.count() - 1
            else:
                print ('pppppppppp')

    def onContextMenu(self, point):
        print ('onCOntextMenu')
        index = self.treeView.indexAt(point)
        chapter = self.tree_model.metadata_model(index, QtCore.Qt.EditRole)
        if index.isValid() :
            self.contextMenu.clear()
            action_add = QAction("ajout sous chapitre", self.treeView)
            action_add.triggered.connect(partial(self.onAddChapter, chapter))
            self.contextMenu.addAction(action_add)
            action_add_page = QAction("ajout Page", self.treeView)
            action_add_page.triggered.connect(partial(self.onAddPage, chapter))
            self.contextMenu.addAction(action_add_page)
            action_remove = QAction("remove", self.treeView)
            action_remove.triggered.connect(partial(self.onRemove, chapter))
            self.contextMenu.addAction(action_remove)
            self.contextMenu.exec_(self.treeView.mapToGlobal(point))

    def onAddChapter(self, chapter):
        print ('onAddChapter')
        if chapter.content != None:
            chapitre = chapter.getParent()
        else:
            chapitre = chapter
        test = Chapitre(chapitre, "undefined")
        chapitre.addChild(test)
        self.load()

    def onRemove (self, chapter):
        print ('onRemove')
        if chapter.parent() != None:
            chapter.parent.children.remove(chapter)
            self.load()
    def onAddPage(self, chapter):
        print ('onAddPage')
        filename = QFileDialog.getOpenFileName(self, caption='Choisir le contenu de la page', directory=Config().instance.settings.path_to_book())
        if filename :
            if chapter.content != None:
                chapitre = chapter.getParent()
            else:
                chapitre = chapter
            print ('chapitre partnt',chapitre.title)
            test = Chapitre(chapitre, "undefined", os.path.basename(filename[0]))
            chapitre.addChild(test)
            self.model.print_()
            self.reload()

    def goPrevious (self):
        print ('goPrevious')
        new_index = max(0, self.stackedWidget.currentIndex() - 1)
        self.stackedWidget.setCurrentIndex(new_index)
        if new_index == 0:
            self.previous.setEnabled(False)
        self.next.setEnabled(True)    

    def goNext (self):
        print ('goNext')
        new_index = min(self.stackedWidget.count() - 1, self.stackedWidget.currentIndex() + 1)
        self.stackedWidget.setCurrentIndex(new_index)
        if new_index == (self.stackedWidget.count() - 1):
            self.next.setEnabled(False)        

        self.previous.setEnabled(True)
            
    def onEdit (self):
        print ('onEdit')
        current = None
        for child in self.model.root.children:
            if len(child.children) != 0:
                for sub in child.children :
                    if sub.indice == self.stackedWidget.currentIndex():
                        current = sub
                        break
            else:
                if child.indice == self.stackedWidget.currentIndex():
                    current = child
                    break
        if current != None :    
            textEdit = HtmlEditor(self)#BookEditWindow(self.stackedWidget.currentIndex(),current.content, self)
            textEdit.fileSaved.connect(self.onUpdatePage)
            textEdit.load(os.path.join(Config().instance.settings.path_to_book(),current.content))
            #textEdit.setWindowModality(QtCore.Qt.ApplicationModal)
            #textEdit.resize(700, 800)
            textEdit.show()

    
    def onUpdatePage (self):
        print ('onUpdatePage')
        widget = self.stackedWidget.currentWidget()
        ind = self.stackedWidget.currentIndex()
        widget.load()
        self.stackedWidget.setCurrentIndex(ind)
    def changeCurrentPage(self, index):
        print ('changeCurrentPage')
        indice = self.tree_model.metadata_indice(index, QtCore.Qt.DecorationRole)
        self.stackedWidget.setCurrentIndex(indice)
コード例 #9
0
class EmbedComicMetadata(InterfaceAction):

    name = 'Embed Comic Metadata'

    # Declare the main action associated with this plugin
    if prefs["main_import"]:
        action_spec = (_L['Import Comic Metadata'], None,
                       _L['Imports the metadata from the comic to calibre'],
                       None)
    else:
        action_spec = (_L['Embed Comic Metadata'], None,
                       _L['Embeds calibres metadata into the comic'], None)

    def genesis(self):
        # menu
        self.menu = QMenu(self.gui)

        # Get the icon for this interface action
        icon = self.get_icon('images/embed_comic_metadata.png')

        # The qaction is automatically created from the action_spec defined
        # above
        self.qaction.setMenu(self.menu)
        self.qaction.setIcon(icon)
        self.qaction.triggered.connect(self.main_menu_triggered)

        # build menu
        self.menu.clear()
        self.build_menu()
        self.toggle_menu_items()

    def build_menu(self):
        for item in config[CONFIG_MENU]["UI_Action_Items"]:
            if item[CONFIG_NAME] == "seperator":
                self.menu.addSeparator()
                continue
            elif item[CONFIG_TRIGGER_ARG]:
                triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self,
                                      item[CONFIG_TRIGGER_ARG])
            else:
                triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self)
            self.menu_action(item[CONFIG_NAME], item[CONFIG_DESCRIPTION],
                             triggerfunc)
        # add configuration entry
        self.menu_action(
            "configure", _L["Configure"],
            partial(self.interface_action_base_plugin.do_user_config,
                    (self.gui)))

    def toggle_menu_items(self):
        for item in config[CONFIG_MENU]["Items"]:
            action = getattr(self, item[CONFIG_NAME])
            action.setVisible(prefs[item[CONFIG_NAME]])

    def main_menu_triggered(self):
        from calibre_plugins.EmbedComicMetadata.main import embed_into_comic, import_to_calibre

        i = prefs["main_import"]
        # Check the preferences for what should be done
        if (i and prefs['read_cbi']
                and prefs['read_cix']) or (not i and prefs['cbi_embed']
                                           and prefs['cix_embed']):
            action = "both"
        elif (i and prefs['read_cbi']) or (not i and prefs['cbi_embed']):
            action = "cbi"
        elif (i and prefs['read_cix']) or (not i and prefs['cix_embed']):
            action = "cix"
        else:
            return error_dialog(self.gui,
                                _L['Cannot update metadata'],
                                _L['No embed format selected'],
                                show=True)

        if i:
            import_to_calibre(self, action)
        else:
            embed_into_comic(self, action)

    def apply_settings(self):
        # In an actual non trivial plugin, you would probably need to
        # do something based on the settings in prefs
        prefs

    def menu_action(self, name, title, triggerfunc):
        action = self.create_menu_action(self.menu,
                                         name,
                                         title,
                                         icon=None,
                                         shortcut=None,
                                         description=None,
                                         triggered=triggerfunc,
                                         shortcut_name=None)
        setattr(self, name, action)

    def get_icon(self, icon_name):
        import os
        from calibre.utils.config import config_dir

        # Check to see whether the icon exists as a Calibre resource
        # This will enable skinning if the user stores icons within a folder like:
        # ...\AppData\Roaming\calibre\resources\images\Plugin Name\
        icon_path = os.path.join(config_dir, 'resources', 'images', self.name,
                                 icon_name.replace('images/', ''))
        if os.path.exists(icon_path):
            pixmap = QPixmap()
            pixmap.load(icon_path)
            return QIcon(pixmap)
        # As we did not find an icon elsewhere, look within our zip resources
        return get_icons(icon_name)
コード例 #10
0
ファイル: MainWindow.py プロジェクト: razman786/tigger
class MainWindow(QMainWindow):

    isUpdated = pyqtSignal(bool)
    hasSkyModel = pyqtSignal(bool)
    hasSelection = pyqtSignal(bool)
    modelChanged = pyqtSignal(object)
    closing = pyqtSignal()
    signalShowMessage = pyqtSignal([str, int], [str])
    signalShowErrorMessage = pyqtSignal([str], [str, int])
    ViewModelColumns = [
        "name", "RA", "Dec", "type", "Iapp", "I", "Q", "U", "V", "RM", "spi",
        "shape"
    ]

    def __init__(self,
                 parent,
                 max_width=None,
                 max_height=None,
                 hide_on_close=False):
        QMainWindow.__init__(self, parent)
        self.signalShowMessage.connect(self.showMessage,
                                       type=Qt.QueuedConnection)
        self.signalShowErrorMessage.connect(self.showErrorMessage,
                                            type=Qt.QueuedConnection)
        self.setWindowIcon(pixmaps.tigger_starface.icon())
        self._currier = PersistentCurrier()
        self.hide()
        # init column constants
        for icol, col in enumerate(self.ViewModelColumns):
            setattr(self, "Column%s" % col.capitalize(), icol)
        # init GUI
        self.setWindowTitle("Tigger")
        self.setWindowIcon(QIcon(pixmaps.purr_logo.pm()))
        # central widget setup
        self.cw = QWidget(self)
        # The actual min width of the control dialog is ~396
        self._ctrl_dialog_min_size = 400  # approx value
        # The actual min width of the profile/zoom windows is ~256
        self._profile_and_zoom_widget_min_size = 300  # approx value
        # set usable screen space (90% of available)
        self.max_width = max_width
        self.max_height = max_height
        self.setCentralWidget(self.cw)
        cwlo = QVBoxLayout(self.cw)
        cwlo.setContentsMargins(5, 5, 5, 5)
        # make splitter
        spl1 = self._splitter1 = QSplitter(Qt.Vertical, self.cw)
        spl1.setOpaqueResize(False)
        cwlo.addWidget(spl1)
        # Create listview of LSM entries
        self.tw = SkyModelTreeWidget(spl1)
        self.tw.hide()

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

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

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

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

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

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

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

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

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

        # message handlers
        self.qerrmsg = QErrorMessage(self)

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

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

    # layout identifiers
    LayoutEmpty = "empty"
    LayoutImage = "image"
    LayoutImageModel = "model"
    LayoutSplit = "split"

    def _getFilenamesFromDropEvent(self, event):
        """Checks if drop event is valid (i.e. contains a local URL to a FITS file), and returns list of filenames contained therein."""
        dprint(1, "drop event:", event.mimeData().text())
        if not event.mimeData().hasUrls():
            dprint(1, "drop event: no urls")
            return None
        filenames = []
        for url in event.mimeData().urls():
            name = str(url.toLocalFile())
            dprint(2, "drop event: name is", name)
            if name and Images.isFITS(name):
                filenames.append(name)
        dprint(2, "drop event: filenames are", filenames)
        return filenames

    def dragEnterEvent(self, event):
        if self._getFilenamesFromDropEvent(event):
            dprint(1, "drag-enter accepted")
            event.acceptProposedAction()
        else:
            dprint(1, "drag-enter rejected")

    def dropEvent(self, event):
        busy = None
        filenames = self._getFilenamesFromDropEvent(event)
        dprint(1, "dropping", filenames)
        if filenames:
            event.acceptProposedAction()
            busy = BusyIndicator()
            for name in filenames:
                self.imgman.loadImage(name)
        if busy is not None:
            busy.reset_cursor()

    def saveSizes(self):
        if self._current_layout is not None:
            dprint(1, "saving sizes for layout", self._current_layout)
            # save main window size and splitter dimensions
            sz = self.size()
            Config.set('%s-main-window-width' % self._current_layout,
                       sz.width())
            Config.set('%s-main-window-height' % self._current_layout,
                       sz.height())
            for spl, name in ((self._splitter1, "splitter1"), (self._splitter2,
                                                               "splitter2")):
                ssz = spl.sizes()
                for i, sz in enumerate(ssz):
                    Config.set(
                        '%s-%s-size%d' % (self._current_layout, name, i), sz)

    def loadSizes(self):
        if self._current_layout is not None:
            dprint(1, "loading sizes for layout", self._current_layout)
            # get main window size and splitter dimensions
            w = Config.getint('%s-main-window-width' % self._current_layout, 0)
            h = Config.getint('%s-main-window-height' % self._current_layout,
                              0)
            dprint(2, "window size is", w, h)
            if not (w and h):
                return None
            self.resize(QSize(w, h))
            for spl, name in (self._splitter1, "splitter1"), (self._splitter2,
                                                              "splitter2"):
                ssz = [
                    Config.getint(
                        '%s-%s-size%d' % (self._current_layout, name, i), -1)
                    for i in (0, 1)
                ]
                dprint(2, "splitter", name, "sizes", ssz)
                if all([sz >= 0 for sz in ssz]):
                    spl.setSizes(ssz)
                else:
                    return None
        return True

    def setLayout(self, layout):
        """Changes the current window layout. Restores sizes etc. from config file."""
        if self._current_layout is layout:
            return
        dprint(1, "switching to layout", layout)
        # save sizes to config file
        self.saveSizes()
        # remove imgman widget from all layouts
        for lo in self._skyplot_stack_lo, self._grouptab_stack_lo:
            if lo.indexOf(self.imgman) >= 0:
                lo.removeWidget(self.imgman)
        # assign it to appropriate parent and parent's layout
        if layout is self.LayoutImage:
            lo = self._skyplot_stack_lo
            self.setMaximumSize(self.max_width, self.max_height)
            self.setBaseSize(self.max_width, self.max_height)
            size_policy = QSizePolicy()
            size_policy.setVerticalPolicy(QSizePolicy.Minimum)
            size_policy.setHorizontalPolicy(QSizePolicy.Expanding)
            self.setSizePolicy(size_policy)
            # set central widget size - workaround for bug #164
            # self.cw.setFixedSize(self.max_width - self._ctrl_dialog_min_size - self._profile_and_zoom_widget_min_size, self.max_height)
            # self.cw.setGeometry(0, self.max_width - self._ctrl_dialog_min_size - self._profile_and_zoom_widget_min_size / 2,
            # self.max_width - self._ctrl_dialog_min_size - self._profile_and_zoom_widget_min_size, self.max_height)
        elif layout is self.LayoutEmpty:
            lo = self._skyplot_stack_lo
        else:
            lo = self._grouptab_stack_lo
        self.imgman.setParent(lo.parentWidget())
        lo.addWidget(self.imgman, 0)
        # show/hide panels
        if layout is self.LayoutEmpty:
            self.tw.hide()
            self.grouptab.hide()
            # self.skyplot.show()
        elif layout is self.LayoutImage:
            self.tw.hide()
            self.grouptab.hide()
            self.skyplot.show()
            # setup dockable state from config file
            if Config.getbool('livezoom-show'):
                self.skyplot._livezoom.setVisible(True)
                self.skyplot._dockable_livezoom.setVisible(True)
                self.addDockWidget(Qt.LeftDockWidgetArea,
                                   self.skyplot._dockable_livezoom)
            if Config.getbool('liveprofile-show'):
                self.skyplot._liveprofile.setVisible(True)
                self.skyplot._dockable_liveprofile.setVisible(True)
                self.addDockWidget(Qt.LeftDockWidgetArea,
                                   self.skyplot._dockable_liveprofile)

            # resize dock areas
            widget_list = self.findChildren(QDockWidget)
            size_list = []
            result = []
            for widget in widget_list:
                if not isinstance(widget.bind_widget, ImageControlDialog):
                    size_list.append(widget.bind_widget.width())
                    result.append(widget)
                    dprint(2, f"{widget} width {widget.width()}")
                    dprint(
                        2,
                        f"{widget} bind_widget width {widget.bind_widget.width()}"
                    )
                    if isinstance(widget.bind_widget, LiveImageZoom):
                        widget.bind_widget.setMinimumWidth(widget.width())
            widget_list = result
            # resize dock areas
            self.resizeDocks(widget_list, size_list, Qt.Horizontal)
        elif layout is self.LayoutImageModel:
            self.tw.show()
            self.grouptab.show()
            self.skyplot.show()
        # reload sizes
        self._current_layout = layout
        if not self.loadSizes():
            dprint(1, "no sizes loaded, setting defaults")
            if layout is self.LayoutEmpty:
                self.resize(QSize(512, 256))
            elif layout is self.LayoutImage:
                self.resize(QSize(512, 512))
                self._splitter2.setSizes([512, 0])
            elif layout is self.LayoutImageModel:
                self.resize(QSize(1024, 512))
                self._splitter1.setSizes([256, 256])
                self._splitter2.setSizes([256, 256])

    def enableUpdates(self, enable=True):
        """Enables updates of the child widgets. Usually called after startup is completed (i.e. all data loaded)"""
        self.skyplot.enableUpdates(enable)
        if enable:
            if self.model:
                self.setLayout(self.LayoutImageModel)
            elif self.imgman.getImages():
                self.setLayout(self.LayoutImage)
            else:
                self.setLayout(self.LayoutEmpty)
            self.show()

    def _showAboutDialog(self):
        if not self._about_dialog:
            self._about_dialog = AboutDialog.AboutDialog(self)
        self._about_dialog.show()

    def addTool(self, name, callback):
        """Adds a tool to the Tools menu"""
        self._tools_menu.addAction(
            name, self._currier.curry(self._callTool, callback))

    def _callTool(self, callback):
        callback(self, self.model)

    def _imagesChanged(self):
        """Called when the set of loaded images has changed"""
        if self.imgman.getImages():
            if self._current_layout is self.LayoutEmpty:
                self.setLayout(self.LayoutImage)
        else:
            if not self.model:
                self.setLayout(self.LayoutEmpty)

    def _selectAll(self):
        if not self.model:
            return
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = True
        self.model.emitSelection(self)
        busy.reset_cursor()

    def _unselectAll(self):
        if not self.model:
            return
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = False
        self.model.emitSelection(self)
        busy.reset_cursor()

    def _selectInvert(self):
        if not self.model:
            return
        busy = BusyIndicator()
        for src in self.model.sources:
            src.selected = not src.selected
        self.model.emitSelection(self)
        busy.reset_cursor()

    def _deleteSelection(self):
        unselected = [src for src in self.model.sources if not src.selected]
        nsel = len(self.model.sources) - len(unselected)
        if QMessageBox.question(
                self, "Delete selection",
                """<P>Really deleted %d selected source(s)?
        %d unselected sources will remain in the model.</P>""" %
            (nsel, len(unselected)), QMessageBox.Ok | QMessageBox.Cancel,
                QMessageBox.Cancel) != QMessageBox.Ok:
            return
        self.model.setSources(unselected)
        self.signalShowMessage[str].emit("""Deleted %d sources""" % nsel)
        self.model.emitUpdate(SkyModel.SkyModel.UpdateAll, origin=self)

    def _showSourceSelector(self):
        TigGUI.Tools.source_selector.show_source_selector(self, self.model)

    def _updateModelSelection(self, num, origin=None):
        """Called when the model selection has been updated."""
        self.hasSelection.emit(bool(num))

    import Tigger.Models.Formats
    _formats = [f[1] for f in Tigger.Models.Formats.listFormatsFull()]

    _load_file_types = [(doc, ["*" + ext for ext in extensions], load)
                        for load, save, doc, extensions in _formats if load]
    _save_file_types = [(doc, ["*" + ext for ext in extensions], save)
                        for load, save, doc, extensions in _formats if save]

    def showMessage(self, msg, time=3000):
        self.statusBar().showMessage(msg, time)

    def showErrorMessage(self, msg, time=3000):
        self.qerrmsg.showMessage(msg)

    def loadImage(self, filename):
        return self.imgman.loadImage(filename)

    def setModel(self, model):
        if model is not None:
            self.modelChanged.emit(model)
        if model:
            self.model = model
            self.hasSkyModel.emit(True)
            self.hasSelection.emit(False)
            self.isUpdated.emit(False)
            self.model.enableSignals()
            self.model.connect("updated", self._indicateModelUpdated)
            self.model.connect("selected", self._updateModelSelection)
            # pass to children
            self.tw.setModel(self.model)
            self.grouptab.setModel(self.model)
            self.skyplot.setModel(self.model)
            # add items to View menu
            self._column_view_menu.clear()
            self.tw.addColumnViewActionsTo(self._column_view_menu)
        else:
            self.model = None
            self.setWindowTitle("Tigger")
            self.hasSelection.emit(False)
            self.isUpdated.emit(False)
            self.hasSkyModel.emit(False)
            self.tw.clear()
            self.grouptab.clear()
            self.skyplot.setModel(None)

    def _openFileCallback(self):
        if not self._open_file_dialog:
            filters = ";;".join([
                "%s (%s)" % (name, " ".join(patterns))
                for name, patterns, func in self._load_file_types
            ])
            dialog = self._open_file_dialog = QFileDialog(
                self, "Open sky model", ".", filters)
            dialog.setFileMode(QFileDialog.ExistingFile)
            dialog.setModal(True)
            dialog.filesSelected['QStringList'].connect(self.openFile)
        self._open_file_dialog.exec_()
        return

    def _mergeFileCallback(self):
        if not self._merge_file_dialog:
            filters = ";;".join([
                "%s (%s)" % (name, " ".join(patterns))
                for name, patterns, func in self._load_file_types
            ])
            dialog = self._merge_file_dialog = QFileDialog(
                self, "Merge in sky model", ".", filters)
            dialog.setFileMode(QFileDialog.ExistingFile)
            dialog.setModal(True)
            dialog.filesSelected['QStringList'].connect(
                self._currier.curry(self.openFile, merge=True))
        self._merge_file_dialog.exec_()
        return

    def openFile(self, _filename=None, _format=None, _merge=False, _show=True):
        # check that we can close existing model
        if not _merge and not self._canCloseExistingModel():
            return False
        if isinstance(_filename, QStringList):
            _filename = _filename[0]
        _filename = str(_filename)
        # try to determine the file type
        filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat(
            _filename, _format)
        if import_func is None:
            self.signalShowErrorMessage.emit(
                """Error loading model file %s: unknown file format""" %
                _filename)
            return
        # try to load the specified file
        busy = BusyIndicator()
        self.signalShowMessage.emit(
            """Reading %s file %s""" % (filetype, _filename), 3000)
        QApplication.flush()
        try:
            model = import_func(_filename)
            model.setFilename(_filename)
        except:
            busy.reset_cursor()
            self.signalShowErrorMessage.emit(
                """Error loading '%s' file %s: %s""" %
                (filetype, _filename, str(sys.exc_info()[1])))
            return
        else:
            # set the layout
            if _show:
                self.setLayout(self.LayoutImageModel)
            # add to content
            if _merge and self.model:
                self.model.addSources(model.sources)
                self.signalShowMessage.emit(
                    """Merged in %d sources from '%s' file %s""" %
                    (len(model.sources), filetype, _filename), 3000)
                self.model.emitUpdate(SkyModel.SkyModel.UpdateAll)
            else:
                print("""Loaded %d sources from '%s' file %s""" %
                      (len(model.sources), filetype, _filename))
                self.signalShowMessage.emit(
                    """Loaded %d sources from '%s' file %s""" %
                    (len(model.sources), filetype, _filename), 3000)
                self._display_filename = os.path.basename(_filename)
                self.setModel(model)
                self._indicateModelUpdated(updated=False)
                # only set self.filename if an export function is available for this format. Otherwise set it to None, so that trying to save
                # the file results in a save-as operation (so that we don't save to a file in an unsupported format).
                self.filename = _filename if export_func else None
        finally:
            busy.reset_cursor()

    def closeEvent(self, event):
        dprint(1, "closing")
        self._exiting = True
        self.saveSizes()
        if not self.closeFile():
            self._exiting = False
            event.ignore()
            return
        self.skyplot.close()
        self.imgman.close()
        self.closing.emit()
        dprint(1, "invoking os._exit(0)")
        os._exit(0)
        QMainWindow.closeEvent(self, event)

    def _canCloseExistingModel(self):
        # save model if modified
        if self.model and self._model_updated:
            res = QMessageBox.question(
                self, "Closing sky model",
                "<P>Model has been modified, would you like to save the changes?</P>",
                QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel,
                QMessageBox.Save)
            if res == QMessageBox.Cancel:
                return False
            elif res == QMessageBox.Save:
                if not self.saveFile(confirm=False, overwrite=True):
                    return False
        # unload model images, unless we are already exiting anyway
        if not self._exiting:
            self.imgman.unloadModelImages()
        return True

    def closeFile(self):
        if not self._canCloseExistingModel():
            return False
        # close model
        self._display_filename = None
        self.setModel(None)
        # set the layout
        self.setLayout(self.LayoutImage if self.imgman.getTopImage() else self.
                       LayoutEmpty)
        return True

    def saveFile(self,
                 filename=None,
                 confirm=False,
                 overwrite=True,
                 non_native=False):
        """Saves file using the specified 'filename'. If filename is None, uses current filename, if
        that is not set, goes to saveFileAs() to open dialog and get a filename.
        If overwrite=False, will ask for confirmation before overwriting an existing file.
        If non_native=False, will ask for confirmation before exporting in non-native format.
        If confirm=True, will ask for confirmation regardless.
        Returns True if saving succeeded, False on error (or if cancelled by user).
        """
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = (filename and str(filename)) or self.filename
        if filename is None:
            return self.saveFileAs()
        else:
            warning = ''
            # try to determine the file type
            filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat(
                filename, None)
            if export_func is None:
                self.signalShowErrorMessage.emit(
                    """Error saving model file %s: unsupported output format"""
                    % filename)
                return
            if os.path.exists(filename) and not overwrite:
                warning += "<P>The file already exists and will be overwritten.</P>"
            if filetype != 'Tigger' and not non_native:
                warning += """<P>Please note that you are exporting the model using the external format '%s'.
              Source types, tags and other model features not supported by this
              format will be omitted during the export.</P>""" % filetype
            # get confirmation
            if confirm or warning:
                dialog = QMessageBox.warning if warning else QMessageBox.question
                if dialog(self, "Saving sky model",
                          "<P>Save model to %s?</P>%s" % (filename, warning),
                          QMessageBox.Save | QMessageBox.Cancel,
                          QMessageBox.Save) != QMessageBox.Save:
                    return False
            busy = BusyIndicator()
            try:
                export_func(self.model, filename)
                self.model.setFilename(filename)
            except:
                busy.reset_cursor()
                self.signalShowErrorMessage.emit(
                    """Error saving model file %s: %s""" %
                    (filename, str(sys.exc_info()[1])))
                return False
            else:
                self.signalShowMessage.emit(
                    """Saved model to file %s""" % filename, 3000)
                self._display_filename = os.path.basename(filename)
                self._indicateModelUpdated(updated=False)
                self.filename = filename
                return True
            finally:
                busy.reset_cursor()

    def saveFileAs(self, filename=None):
        """Saves file using the specified 'filename'. If filename is None, opens dialog to get a filename.
        Returns True if saving succeeded, False on error (or if cancelled by user).
        """
        if filename is None:
            if not self._save_as_dialog:
                filters = ";;".join([
                    "%s (%s)" % (name, " ".join(patterns))
                    for name, patterns, func in self._save_file_types
                ])
                dialog = self._save_as_dialog = QFileDialog(
                    self, "Save sky model", ".", filters)
                dialog.setDefaultSuffix(ModelHTML.DefaultExtension)
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setOption(QFileDialog.DontConfirmOverwrite, True)
                dialog.setModal(True)
                dialog.filesSelected['QStringList'].connect(self.saveFileAs)
            return self._save_as_dialog.exec_() == QDialog.Accepted
        # filename supplied, so save
        return self.saveFile(filename, confirm=False)

    def saveSelectionAs(self, filename=None, force=False):
        if not self.model:
            return
        if filename is None:
            if not self._save_sel_as_dialog:
                filters = ";;".join([
                    "%s (%s)" % (name, " ".join(patterns))
                    for name, patterns, func in self._save_file_types
                ])
                dialog = self._save_sel_as_dialog = QFileDialog(
                    self, "Save sky model", ".", filters)
                dialog.setDefaultSuffix(ModelHTML.DefaultExtension)
                dialog.setFileMode(QFileDialog.AnyFile)
                dialog.setAcceptMode(QFileDialog.AcceptSave)
                dialog.setOption(QFileDialog.DontConfirmOverwrite, False)
                dialog.setModal(True)
                dialog.filesSelected['QStringList'].connect(
                    self.saveSelectionAs)
            return self._save_sel_as_dialog.exec_() == QDialog.Accepted
        # save selection
        if isinstance(filename, QStringList):
            filename = filename[0]
        filename = str(filename)
        selmodel = self.model.copy()
        sources = [src for src in self.model.sources if src.selected]
        if not sources:
            self.signalShowErrorMessage.emit(
                """You have not selected any sources to save.""")
            return
        # try to determine the file type
        filetype, import_func, export_func, doc = Tigger.Models.Formats.resolveFormat(
            filename, None)
        if export_func is None:
            self.signalShowErrorMessage.emit(
                """Error saving model file %s: unsupported output format""" %
                filename)
            return
        busy = BusyIndicator()
        try:
            export_func(self.model, filename, sources=sources)
        except:
            busy.reset_cursor()
            self.signalShowErrorMessage.emit(
                """Error saving selection to model file %s: %s""" %
                (filename, str(sys.exc_info()[1])))
            return False
        else:
            self.signalShowMessage.emit(
                """Wrote %d selected source%s to file %s""" %
                (len(selmodel.sources),
                 "" if len(selmodel.sources) == 1 else "s", filename), 3000)
        finally:
            busy.reset_cursor()
        pass

    def addTagToSelection(self):
        if not hasattr(self, '_add_tag_dialog'):
            self._add_tag_dialog = Widgets.AddTagDialog(self, modal=True)
        self._add_tag_dialog.setTags(self.model.tagnames)
        self._add_tag_dialog.setValue(True)
        if self._add_tag_dialog.exec_() != QDialog.Accepted:
            return
        tagname, value = self._add_tag_dialog.getTag()
        if tagname is None or value is None:
            return None
        dprint(1, "tagging selected sources with", tagname, value)
        # tag selected sources
        for src in self.model.sources:
            if src.selected:
                src.setAttribute(tagname, value)
        # If tag is not new, set a UpdateSelectionOnly flag on the signal
        dprint(1, "adding tag to model")
        self.model.addTag(tagname)
        dprint(1, "recomputing totals")
        self.model.getTagGrouping(tagname).computeTotal(self.model.sources)
        dprint(1, "emitting update signal")
        what = SkyModel.SkyModel.UpdateSourceContent + SkyModel.SkyModel.UpdateTags + SkyModel.SkyModel.UpdateSelectionOnly
        self.model.emitUpdate(what, origin=self)

    def removeTagsFromSelection(self):
        if not hasattr(self, '_remove_tag_dialog'):
            self._remove_tag_dialog = Widgets.SelectTagsDialog(
                self, modal=True, caption="Remove Tags", ok_button="Remove")
        # get set of all tags in selected sources
        tags = set()
        for src in self.model.sources:
            if src.selected:
                tags.update(src.getTagNames())
        if not tags:
            return
        tags = list(tags)
        tags.sort()
        # show dialog
        self._remove_tag_dialog.setTags(tags)
        if self._remove_tag_dialog.exec_() != QDialog.Accepted:
            return
        tags = self._remove_tag_dialog.getSelectedTags()
        if not tags:
            return
        # ask for confirmation
        plural = (len(tags) > 1 and "s") or ""
        if QMessageBox.question(
                self, "Removing tags",
                "<P>Really remove the tag%s '%s' from selected sources?</P>" %
            (plural, "', '".join(tags)), QMessageBox.Yes | QMessageBox.No,
                QMessageBox.Yes) != QMessageBox.Yes:
            return
        # remove the tags
        for src in self.model.sources:
            if src.selected:
                for tag in tags:
                    src.removeAttribute(tag)
        # update model
        self.model.scanTags()
        self.model.initGroupings()
        # emit signal
        what = SkyModel.SkyModel.UpdateSourceContent + SkyModel.SkyModel.UpdateTags + SkyModel.SkyModel.UpdateSelectionOnly
        self.model.emitUpdate(what, origin=self)

    def _indicateModelUpdated(self, what=None, origin=None, updated=True):
        """Marks model as updated."""
        self._model_updated = updated
        self.isUpdated.emit(updated)
        if self.model:
            self.setWindowTitle("Tigger - %s%s" %
                                ((self._display_filename or "(unnamed)",
                                  " (modified)" if updated else "")))