Example #1
0
    def __init__(self, dbmanager, parent=None):
        super(QtGui.QWidget, self).__init__(parent)
        self.setupUi(self)

        self.setWindowTitle('Image Editing v' + ef.__version__)

        self.settings = QtCore.QSettings()

        self.main_stack.setCurrentIndex(0)

        self.username = self.settings.value('ef-username', '').toString()
        self.ef_username.setText(self.username)
        self.password = ''
        self.try_login = TryLogin()
        self.try_login.completed.connect(self.try_login_completed)
        self.try_login.error.connect(self.try_login_error)
        self.ef_login.clicked.connect(self.try_ef_login)
        self.ef_password.returnPressed.connect(self.try_ef_login)

        self.history_make_current.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ArrowRight))
        self.back.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ArrowBack))
        self.forwards.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ArrowForward))
        self.back.setDisabled(True)
        self.forwards.setDisabled(True)

        self.photodownloader = PhotoDownloader()
        self.list_photo_cache = ThumbnailCache(self.photodownloader, 100)
        self.main_photo_cache = PhotoImageCache(self.photodownloader, 10)
        self.fetcher = Fetcher()
        self.reportsfetcher = ReportsFetcher()
        self.uploader = Uploader()
        self.current_person = None
        self.current_photo = None
        self.current_image = None
        self.loading_now = False
        self.photo_load_failed = False
        self.registration_loaded = self.person_loaded = False

        self.history_back = deque()
        self.history_forwards = deque()
        self.suppress_history = False

        self.upload_wizard = UploadPeopleWizard()

        self.image_list_items = {}

        self.edit_scene = QtGui.QGraphicsScene(self)
        self.main_image.setScene(self.edit_scene)

        self.main_pixmap = EditPixmap(self.wheel_event)
        self.edit_scene.addItem(self.main_pixmap)

        self.crop_frame = CropFrame(self.main_pixmap, self.output_updated)

        self.main_pixmap.setZValue(1)
        self.crop_frame.setZValue(2)

        self.events = {}
        self.event_load_handlers = {}

        self.person_model = QtGui.QStandardItemModel(self)
        self.person_model.setColumnCount(1)

        self.person_model_proxy = FilterProxyModel()
        self.person_model_proxy.setDynamicSortFilter(True)
        self.person_model_proxy.setSourceModel(self.person_model)
        self.person_model_proxy.setSortCaseSensitivity(False)
        self.person_model_proxy.setSortRole(QtCore.Qt.UserRole+1)
        self.person_model_proxy.sort(0)

        self.person_model_proxy.rowsInserted.connect(self.handle_filter_count)
        self.person_model_proxy.rowsRemoved.connect(self.handle_filter_count)
        self.person_model_proxy.modelReset.connect(self.handle_filter_count)

        self.filter_opinion.currentIndexChanged[str].connect(self.person_model_proxy.set_opinion)
        self.filter_DNU.currentIndexChanged[str].connect(self.person_model_proxy.set_DNU)
        self.filter_event.currentIndexChanged[int].connect(self.handle_filter_event_changed)
        self.filter_category.currentIndexChanged[str].connect(self.person_model_proxy.set_category)
        self.filter_police.currentIndexChanged[str].connect(self.person_model_proxy.set_police_status)
        self.filter_by_size.stateChanged.connect(self.person_model_proxy.set_only_bad_sizes)
        self.filter_only_missing.stateChanged.connect(self.person_model_proxy.set_only_missing)
        self.filter_only_upload.stateChanged.connect(self.person_model_proxy.set_only_upload)

        self.filters_reset.clicked.connect(self.handle_reset_filters)
        self.export_people.clicked.connect(self.handle_export_people)

        self.history_model = QtGui.QStandardItemModel(self)
        self.history_model.setColumnCount(1)

        self.history_list.setModel(self.history_model)
        self.history_items = {}
        self.history_make_current.clicked.connect(self.handle_historymakecurrent)

        self.dbmanager = dbmanager
        dbmanager.created.connect(self.handle_db_created)
        dbmanager.exception.connect(self.handle_db_exception)
        dbmanager.existing_done.connect(self.handle_db_existing_done)
        dbmanager.process_done.connect(self.handle_db_process_done)
        Photo.signal_existing_created()
        Registration.signal_existing_created()
        Event.signal_existing_created()
        Person.signal_existing_created()

        self.output_updated.connect(self.handle_crop)
        self.wheel_event.connect(self.crop_frame.handle_wheel)
        self.opinion_ok.clicked.connect(self.handle_opinion_ok)
        self.opinion_bad.clicked.connect(self.handle_opinion_bad)
        self.opinion_unsure.clicked.connect(self.handle_opinion_unsure)
        self.do_not_upload.stateChanged.connect(self.handle_do_not_upload)
        self.back.clicked.connect(self.handle_back)
        self.forwards.clicked.connect(self.handle_forwards)
        self.search.clicked.connect(self.handle_search)
        self.search_for.returnPressed.connect(self.handle_search)

        self.action_fetch.triggered.connect(self.handle_fetch_wizard)
        self.fetch_this_person.clicked.connect(self.handle_fetch_person)

        self.fetcher.completed.connect(self.handle_fetch_completed)
        self.fetcher.error.connect(self.handle_fetch_error)
        self.fetcher.progress.connect(self.handle_fetch_progress)

        self.reportsfetcher.error.connect(self.handle_reportsfetch_error)

        self.action_upload.triggered.connect(self.handle_upload_wizard)
        self.upload_wizard.accepted.connect(self.handle_upload)
        self.upload_wizard.rejected.connect(self.handle_upload_rejected)

        self.uploader.completed.connect(self.handle_upload_completed)
        self.uploader.error.connect(self.handle_upload_error)
        self.uploader.progress.connect(self.handle_upload_progress)

        self.rotate.valueChanged.connect(self.handle_rotate)
        self.rotate_0.clicked.connect(lambda: self.rotate.setValue(0))
        self.rotate_l90.clicked.connect(lambda: self.rotate.setValue(-90))
        self.rotate_l180.clicked.connect(lambda: self.rotate.setValue(-180))
        self.rotate_r90.clicked.connect(lambda: self.rotate.setValue(90))
        self.rotate_r180.clicked.connect(lambda: self.rotate.setValue(180))
        self.brightness_slider.valueChanged.connect(self.handle_brightness)
        self.contrast_slider.valueChanged.connect(self.handle_contrast)

        self.gamma_slider.valueChanged.connect(lambda v: self.gamma_spin.setValue(v/10))
        self.gamma_spin.valueChanged.connect(lambda v: self.gamma_slider.setValue(v*10))
        self.gamma_slider.valueChanged.connect(self.handle_gamma)

        self.action_openeventsforce.triggered.connect(self.handle_openeventsforce)
        self.openeventsforce.clicked.connect(self.handle_openeventsforce)
        self.action_reloadphoto.triggered.connect(self.handle_reloadphoto)
        self.action_editimage.triggered.connect(self.handle_editimage)
        self.editimage.clicked.connect(self.handle_editimage)
        self.action_importphoto.triggered.connect(self.handle_import_photo)

        self.openimage = QtGui.QFileDialog(self, 'Import image')
        self.openimage.setFileMode(QtGui.QFileDialog.ExistingFile)
        self.openimage.setNameFilter('*.jpg *.jpeg')
        self.openimage.restoreState(self.settings.value('openimage-state', '').toByteArray())

        self.action_chooseeditor.triggered.connect(self.handle_chooseeditor)

        self.chooseeditor = QtGui.QFileDialog(self, 'Choose image editor')
        self.chooseeditor.setFileMode(QtGui.QFileDialog.ExistingFile)
        if os.name == 'nt':
            self.chooseeditor.setNameFilter('*.exe')
        self.chooseeditor.restoreState(self.settings.value('chooseeditor-state', '').toByteArray())

        self.savereport = QtGui.QFileDialog(self, 'Save report')
        self.savereport.setFileMode(QtGui.QFileDialog.AnyFile)
        self.savereport.setAcceptMode(QtGui.QFileDialog.AcceptSave)
        self.savereport.setNameFilter('*.csv')
        self.savereport.setDefaultSuffix('csv')
        self.savereport.restoreState(self.settings.value('savereport-state', '').toByteArray())

        self.image_editor = self.settings.value('image-editor', '').toString()

        self.action_export.triggered.connect(self.handle_export)
        self.action_import.triggered.connect(self.handle_import)

        self.saveexport = QtGui.QFileDialog(self, 'Export database')
        self.saveexport.setFileMode(QtGui.QFileDialog.AnyFile)
        self.saveexport.setAcceptMode(QtGui.QFileDialog.AcceptSave)
        self.saveexport.setNameFilter('*.yaml')
        self.saveexport.setDefaultSuffix('yaml')
        self.saveexport.restoreState(self.settings.value('saveexport-state', '').toByteArray())

        self.openimport = QtGui.QFileDialog(self, 'Import database')
        self.openimport.setFileMode(QtGui.QFileDialog.ExistingFile)
        self.openimport.setNameFilter('*.yaml')
        self.openimport.restoreState(self.settings.value('openimport-state', '').toByteArray())

        self.status_expiry_timer = QtCore.QTimer(self)
        self.status_expiry_timer.setInterval(5000)
        self.status_expiry_timer.timeout.connect(self.status_idle)

        self.status_task = ''
        self.status_started = None
        self.status_timer = QtCore.QTimer(self)
        self.status_timer.setInterval(500)
        self.status_timer.timeout.connect(self.status_timer_update)
        self.status_is_idle = True

        self.draw_timer = QtCore.QTimer(self)
        self.draw_timer.setInterval(50)
        self.draw_timer.timeout.connect(self.handle_draw)
        self.draw_timer.start()
        self.image_draw_needed = False

        self.photodownloader.queue_size.connect(self.status_downloader)
        self.photodownloader.error.connect(self.photodownload_error)

        self.procs = {}

        self.status_idle()
Example #2
0
class ImageEdit(QtGui.QMainWindow, Ui_ImageEdit):
    output_updated = QtCore.pyqtSignal()
    wheel_event = QtCore.pyqtSignal(int)

    def __init__(self, dbmanager, parent=None):
        super(QtGui.QWidget, self).__init__(parent)
        self.setupUi(self)

        self.setWindowTitle('Image Editing v' + ef.__version__)

        self.settings = QtCore.QSettings()

        self.main_stack.setCurrentIndex(0)

        self.username = self.settings.value('ef-username', '').toString()
        self.ef_username.setText(self.username)
        self.password = ''
        self.try_login = TryLogin()
        self.try_login.completed.connect(self.try_login_completed)
        self.try_login.error.connect(self.try_login_error)
        self.ef_login.clicked.connect(self.try_ef_login)
        self.ef_password.returnPressed.connect(self.try_ef_login)

        self.history_make_current.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ArrowRight))
        self.back.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ArrowBack))
        self.forwards.setIcon(self.style().standardIcon(QtGui.QStyle.SP_ArrowForward))
        self.back.setDisabled(True)
        self.forwards.setDisabled(True)

        self.photodownloader = PhotoDownloader()
        self.list_photo_cache = ThumbnailCache(self.photodownloader, 100)
        self.main_photo_cache = PhotoImageCache(self.photodownloader, 10)
        self.fetcher = Fetcher()
        self.reportsfetcher = ReportsFetcher()
        self.uploader = Uploader()
        self.current_person = None
        self.current_photo = None
        self.current_image = None
        self.loading_now = False
        self.photo_load_failed = False
        self.registration_loaded = self.person_loaded = False

        self.history_back = deque()
        self.history_forwards = deque()
        self.suppress_history = False

        self.upload_wizard = UploadPeopleWizard()

        self.image_list_items = {}

        self.edit_scene = QtGui.QGraphicsScene(self)
        self.main_image.setScene(self.edit_scene)

        self.main_pixmap = EditPixmap(self.wheel_event)
        self.edit_scene.addItem(self.main_pixmap)

        self.crop_frame = CropFrame(self.main_pixmap, self.output_updated)

        self.main_pixmap.setZValue(1)
        self.crop_frame.setZValue(2)

        self.events = {}
        self.event_load_handlers = {}

        self.person_model = QtGui.QStandardItemModel(self)
        self.person_model.setColumnCount(1)

        self.person_model_proxy = FilterProxyModel()
        self.person_model_proxy.setDynamicSortFilter(True)
        self.person_model_proxy.setSourceModel(self.person_model)
        self.person_model_proxy.setSortCaseSensitivity(False)
        self.person_model_proxy.setSortRole(QtCore.Qt.UserRole+1)
        self.person_model_proxy.sort(0)

        self.person_model_proxy.rowsInserted.connect(self.handle_filter_count)
        self.person_model_proxy.rowsRemoved.connect(self.handle_filter_count)
        self.person_model_proxy.modelReset.connect(self.handle_filter_count)

        self.filter_opinion.currentIndexChanged[str].connect(self.person_model_proxy.set_opinion)
        self.filter_DNU.currentIndexChanged[str].connect(self.person_model_proxy.set_DNU)
        self.filter_event.currentIndexChanged[int].connect(self.handle_filter_event_changed)
        self.filter_category.currentIndexChanged[str].connect(self.person_model_proxy.set_category)
        self.filter_police.currentIndexChanged[str].connect(self.person_model_proxy.set_police_status)
        self.filter_by_size.stateChanged.connect(self.person_model_proxy.set_only_bad_sizes)
        self.filter_only_missing.stateChanged.connect(self.person_model_proxy.set_only_missing)
        self.filter_only_upload.stateChanged.connect(self.person_model_proxy.set_only_upload)

        self.filters_reset.clicked.connect(self.handle_reset_filters)
        self.export_people.clicked.connect(self.handle_export_people)

        self.history_model = QtGui.QStandardItemModel(self)
        self.history_model.setColumnCount(1)

        self.history_list.setModel(self.history_model)
        self.history_items = {}
        self.history_make_current.clicked.connect(self.handle_historymakecurrent)

        self.dbmanager = dbmanager
        dbmanager.created.connect(self.handle_db_created)
        dbmanager.exception.connect(self.handle_db_exception)
        dbmanager.existing_done.connect(self.handle_db_existing_done)
        dbmanager.process_done.connect(self.handle_db_process_done)
        Photo.signal_existing_created()
        Registration.signal_existing_created()
        Event.signal_existing_created()
        Person.signal_existing_created()

        self.output_updated.connect(self.handle_crop)
        self.wheel_event.connect(self.crop_frame.handle_wheel)
        self.opinion_ok.clicked.connect(self.handle_opinion_ok)
        self.opinion_bad.clicked.connect(self.handle_opinion_bad)
        self.opinion_unsure.clicked.connect(self.handle_opinion_unsure)
        self.do_not_upload.stateChanged.connect(self.handle_do_not_upload)
        self.back.clicked.connect(self.handle_back)
        self.forwards.clicked.connect(self.handle_forwards)
        self.search.clicked.connect(self.handle_search)
        self.search_for.returnPressed.connect(self.handle_search)

        self.action_fetch.triggered.connect(self.handle_fetch_wizard)
        self.fetch_this_person.clicked.connect(self.handle_fetch_person)

        self.fetcher.completed.connect(self.handle_fetch_completed)
        self.fetcher.error.connect(self.handle_fetch_error)
        self.fetcher.progress.connect(self.handle_fetch_progress)

        self.reportsfetcher.error.connect(self.handle_reportsfetch_error)

        self.action_upload.triggered.connect(self.handle_upload_wizard)
        self.upload_wizard.accepted.connect(self.handle_upload)
        self.upload_wizard.rejected.connect(self.handle_upload_rejected)

        self.uploader.completed.connect(self.handle_upload_completed)
        self.uploader.error.connect(self.handle_upload_error)
        self.uploader.progress.connect(self.handle_upload_progress)

        self.rotate.valueChanged.connect(self.handle_rotate)
        self.rotate_0.clicked.connect(lambda: self.rotate.setValue(0))
        self.rotate_l90.clicked.connect(lambda: self.rotate.setValue(-90))
        self.rotate_l180.clicked.connect(lambda: self.rotate.setValue(-180))
        self.rotate_r90.clicked.connect(lambda: self.rotate.setValue(90))
        self.rotate_r180.clicked.connect(lambda: self.rotate.setValue(180))
        self.brightness_slider.valueChanged.connect(self.handle_brightness)
        self.contrast_slider.valueChanged.connect(self.handle_contrast)

        self.gamma_slider.valueChanged.connect(lambda v: self.gamma_spin.setValue(v/10))
        self.gamma_spin.valueChanged.connect(lambda v: self.gamma_slider.setValue(v*10))
        self.gamma_slider.valueChanged.connect(self.handle_gamma)

        self.action_openeventsforce.triggered.connect(self.handle_openeventsforce)
        self.openeventsforce.clicked.connect(self.handle_openeventsforce)
        self.action_reloadphoto.triggered.connect(self.handle_reloadphoto)
        self.action_editimage.triggered.connect(self.handle_editimage)
        self.editimage.clicked.connect(self.handle_editimage)
        self.action_importphoto.triggered.connect(self.handle_import_photo)

        self.openimage = QtGui.QFileDialog(self, 'Import image')
        self.openimage.setFileMode(QtGui.QFileDialog.ExistingFile)
        self.openimage.setNameFilter('*.jpg *.jpeg')
        self.openimage.restoreState(self.settings.value('openimage-state', '').toByteArray())

        self.action_chooseeditor.triggered.connect(self.handle_chooseeditor)

        self.chooseeditor = QtGui.QFileDialog(self, 'Choose image editor')
        self.chooseeditor.setFileMode(QtGui.QFileDialog.ExistingFile)
        if os.name == 'nt':
            self.chooseeditor.setNameFilter('*.exe')
        self.chooseeditor.restoreState(self.settings.value('chooseeditor-state', '').toByteArray())

        self.savereport = QtGui.QFileDialog(self, 'Save report')
        self.savereport.setFileMode(QtGui.QFileDialog.AnyFile)
        self.savereport.setAcceptMode(QtGui.QFileDialog.AcceptSave)
        self.savereport.setNameFilter('*.csv')
        self.savereport.setDefaultSuffix('csv')
        self.savereport.restoreState(self.settings.value('savereport-state', '').toByteArray())

        self.image_editor = self.settings.value('image-editor', '').toString()

        self.action_export.triggered.connect(self.handle_export)
        self.action_import.triggered.connect(self.handle_import)

        self.saveexport = QtGui.QFileDialog(self, 'Export database')
        self.saveexport.setFileMode(QtGui.QFileDialog.AnyFile)
        self.saveexport.setAcceptMode(QtGui.QFileDialog.AcceptSave)
        self.saveexport.setNameFilter('*.yaml')
        self.saveexport.setDefaultSuffix('yaml')
        self.saveexport.restoreState(self.settings.value('saveexport-state', '').toByteArray())

        self.openimport = QtGui.QFileDialog(self, 'Import database')
        self.openimport.setFileMode(QtGui.QFileDialog.ExistingFile)
        self.openimport.setNameFilter('*.yaml')
        self.openimport.restoreState(self.settings.value('openimport-state', '').toByteArray())

        self.status_expiry_timer = QtCore.QTimer(self)
        self.status_expiry_timer.setInterval(5000)
        self.status_expiry_timer.timeout.connect(self.status_idle)

        self.status_task = ''
        self.status_started = None
        self.status_timer = QtCore.QTimer(self)
        self.status_timer.setInterval(500)
        self.status_timer.timeout.connect(self.status_timer_update)
        self.status_is_idle = True

        self.draw_timer = QtCore.QTimer(self)
        self.draw_timer.setInterval(50)
        self.draw_timer.timeout.connect(self.handle_draw)
        self.draw_timer.start()
        self.image_draw_needed = False

        self.photodownloader.queue_size.connect(self.status_downloader)
        self.photodownloader.error.connect(self.photodownload_error)

        self.procs = {}

        self.status_idle()

    def status_idle(self):
        self.progress.reset()
        self.progress.hide()
        self.status_is_idle = True
        self.status_downloader()

    def status_downloader(self):
        if self.status_is_idle:
            downloads = self.photodownloader.get_queue_size()
            if downloads > 0:
                self.status.setText('Downloading %d images' % downloads)
            else:
                self.status.setText('Idle')

    def status_start(self, task, max):
        self.status_expiry_timer.stop()
        self.progress.setRange(0, max)
        self.progress.show()
        self.status_is_idle = False

        if max == 0:
            if self.status_started is None:
                self.status_started = datetime.now()
                self.status_timer.start()
            self.status_task = task
            self.status_timer_update()
        else:
            self.status_timer.stop()
            self.status_started = None
            self.status.setText(task)

    def status_elapsed_str(self):
        td = datetime.now() - self.status_started
        minutes,seconds = divmod(td.seconds, 60)
        return "%d:%02d" % (minutes,seconds)

    def status_timer_update(self):
        if self.status_started is not None:
            self.status.setText("%s (%s)" % (self.status_task, self.status_elapsed_str())) 

    def status_finishing(self):
        if self.status_started is not None:
            self.status_timer.stop()
            self.status.setText("%s (finished in %s)" % (self.status_task, self.status_elapsed_str())) 

    def status_finished(self):
        if self.status_started is not None:
            if self.status_timer.isActive():
                self.status_timer.stop()
                self.status.setText("%s (finished in %s)" % (self.status_task, self.status_elapsed_str())) 
            self.progress.setRange(0, 1)
            self.progress.setValue(1)
            self.status_started = None
        else:
            self.progress.setValue(self.progress.maximum())

        timer = QtCore.QTimer(self)
        self.status_expiry_timer.stop()
        self.status_expiry_timer.setSingleShot(True)
        self.status_expiry_timer.start()

    def clear_image(self):
        self.current_photo = None
        self.current_image = None
        self.image_draw_needed = False
        self.crop_frame.hide()
        self.main_pixmap.hide()
        self.preview_image.setPixmap(QtGui.QPixmap())
        self.person_name.setText(u'')
        self.upload_wizard.upload_photos_thisname.setText('')

    def foreach_item(self, f):
        for item in self.image_list_items.itervalues():
            f(item)

    def handle_search(self):
        query = unicode(self.search_for.text()).strip()

        # If the search box is empty, we want to show all items
        if len(query) == 0:
            self.person_model_proxy.id = None
            self.person_model_proxy.name = ''
            self.person_model_proxy.invalidateFilter()
            self.person_model_proxy.sort(0)
            return

        # If the query is just a number, treat it as a person ID query
        try:
            id = int(query)
        except ValueError:
            id = None

        if id is not None:
            self.person_model_proxy.id = id
        else:
            self.person_model_proxy.id = None
            self.person_model_proxy.name = query
        self.person_model_proxy.invalidateFilter()
        self.person_model_proxy.sort(0)

    def item_from_index(self, index):
        if index.isValid():
            index = self.person_model_proxy.mapToSource(index)
        if index.isValid():
            return self.person_model.itemFromIndex(index)
        else:
            return None

    def handle_select(self, current, previous):
        current = self.item_from_index(current)
        previous = self.item_from_index(previous)

        if current is None:
            return

        self.photo_load_failed = False
        person_id = current.data(QtCore.Qt.UserRole)
        self.load_person(person_id)

    def handle_model_item_changed(self, item):
        changed_index = self.person_model_proxy.mapFromSource(item.index())
        current_index = self.person_list.selectionModel().currentIndex()

        # If this item is the currently selected item...
        if changed_index.isValid() and current_index.isValid() and changed_index == current_index:
            # ...fish out the relevant data...
            person_id = item.data(QtCore.Qt.UserRole)

            p = self.image_list_items[person_id].person
            photo = self.image_list_items[person_id].photo

            # ...and see if it's the same thing we've already loaded
            if (self.current_person is not None and self.current_photo is not None
                and p.id == self.current_person.id and photo.id == self.current_photo.id):
                # If it's the same, just redraw the image to pick up photo control changes
                self.image_draw_needed = True
            else:
                # For anything else just reload the person
                self.load_person(person_id)

    """Load this person's photo into the editor"""
    def load_person(self, id, refresh=False):
        if self.photo_load_failed:
            return

        p = self.image_list_items[id].person
        photo = self.image_list_items[id].photo

        # Suppress history updates when changing the item with
        # back/forwards, because they have special handling
        if not self.suppress_history and self.current_photo is not None and self.current_person.id != id:
            self.history_back.append(self.current_person.id)
            self.back.setDisabled(False)
            self.history_forwards.clear()
            self.forwards.setDisabled(True)
            while len(self.history_back) > 10:
                self.history_back.popleft()

        self.clear_image()
        self.history_model.clear()
        self.current_person = p

        self.info_person_id.setText('%d' % self.current_person.id)
        self.info_fullname.setText(self.current_person.fullname)
        self.info_title.setText(self.current_person.title)
        self.info_firstname.setText(self.current_person.firstname)
        self.info_lastname.setText(self.current_person.lastname)
        self.info_police_status.setText(self.current_person.police_status)
        self.info_person_fetched_at.setText(time.ctime(self.current_person.last_checked_at))

        if photo is not None and photo.full_path() is not None:
            self.current_photo = photo
            filename = self.current_photo.local_filename()
            self.info_photo_filename.setText(filename if filename else '')
            self.info_photo_fetched_at.setText(time.ctime(self.current_photo.date_fetched))
            self.person_name.setText(u'Loading %s...' % p)
            self.upload_wizard.upload_photos_thisname.setText(unicode(p))

            self.main_photo_cache.load_image(photo.id, photo.full_path(), photo.url,
                                             ready_cb=self.handle_photo_ready,
                                             fail_cb=self.handle_photo_fail,
                                             urgent=True, refresh=refresh,
                                             )
        else:
            self.person_name.setText(unicode(self.current_person))

        photos = Photo.by_person(self.current_person.id)
        self.history_model.clear()
        self.history_items = {}
        for photo in sorted(photos, key=lambda photo: photo.date_fetched, reverse=True):
            item = QtGui.QStandardItem()
            msg = "Fetched at %s" % time.ctime(photo.date_fetched)
            if self.current_person.current_photo_id == photo.id:
                msg = msg + "\nCurrent photo"
            else:
                msg = msg + "\n%s" % photo.opinion
            item.setText(msg)
            item.setTextAlignment(QtCore.Qt.AlignCenter)
            item.setData(photo.id)
            self.history_items[photo.id] = item
            self.history_model.appendRow(item)
            self.list_photo_cache.load_image(photo.id, photo.full_path(), photo.url,
                                             ready_cb=self.handle_history_photo_ready)

    def handle_history_photo_ready(self, photo_id, pixmap):
        item = self.history_items.get(photo_id, None)
        if item is not None:
            item.setData(pixmap, QtCore.Qt.DecorationRole)

    def handle_historymakecurrent(self):
        if not self.current_person:
            return
        item = self.history_model.itemFromIndex(self.history_list.currentIndex())
        if item is None:
            return
        photo_id = item.data().toPyObject()
        if photo_id != self.current_person.current_photo_id:
            self.current_person.update_current_photo(photo_id)

    def handle_reloadphoto(self):
        if self.current_person is not None:
            self.load_person(self.current_person.id, refresh=True)        

    """Select this id in person_list"""
    def select_person(self, id):
        index = self.image_list_items[id].index()
        if not index.isValid():
            return False
        index = self.person_model_proxy.mapFromSource(index)
        if not index.isValid():
            return False
        self.person_list.selectionModel().setCurrentIndex(index, QtGui.QItemSelectionModel.ClearAndSelect)
        return True

    """Get us to this id, in whatever way makes sense"""
    def jump_to_person(self, id):
        if not self.select_person(id):
            self.load_person(id)

    def handle_back(self):
        if self.current_person is not None:
            self.history_forwards.append(self.current_person.id)
            self.forwards.setDisabled(False)
        if len(self.history_back) > 0:
            id = self.history_back.pop()
            self.suppress_history = True
            self.jump_to_person(id)
            self.suppress_history = False
        if len(self.history_back) == 0:
            self.back.setDisabled(True)

    def handle_forwards(self):
        if self.current_person is not None:
            self.history_back.append(self.current_person.id)
            self.back.setDisabled(False)
        if len(self.history_forwards) > 0:
            id = self.history_forwards.pop()
            self.suppress_history = True
            self.jump_to_person(id)
            self.suppress_history = False
        if len(self.history_forwards) == 0:
            self.forwards.setDisabled(True)

    def handle_photo_fail(self, id, error):
        if self.current_photo is not None and self.current_photo.id == id:
            text = u'Failed to load %s: %s' % (self.current_person, error)
            self.clear_image()
            self.person_name.setText(text)
            self.photo_load_failed = True

    def handle_photo_ready(self, id, image):        
        if self.current_photo is None or self.current_photo.id != id:
            return

        angle = self.current_photo.rotate
        brightness = self.current_photo.brightness
        contrast = self.current_photo.contrast
        gamma = self.current_photo.gamma

        self.rotate.setValue(angle)
        self.brightness_slider.setValue(brightness*127)
        self.contrast_slider.setValue(contrast*127)
        self.gamma_slider.setValue(gamma*10)

        image.set_rotation(angle)
        image.set_brightness(brightness)
        image.set_contrast(contrast)
        image.set_gamma(gamma)

        self.current_image = image
        self.image_draw_needed = True

    def handle_draw(self):
        if self.image_draw_needed and self.current_image is not None:
            self.setup_photo()

    def setup_photo(self):
        # Suppress re-entrant noise, because loading a photo emits
        # signals like editing would
        self.loading_now = True

        image = self.current_image.make_qimage()
        pixmap = QtGui.QPixmap.fromImage(image.copy())
        self.main_pixmap.setPixmap(pixmap)

        width = pixmap.width()
        height = pixmap.height()
        self.main_pixmap.setTransformOriginPoint(width/2, height/2)
        self.edit_scene.setSceneRect(0, 0, width, height)
        self.crop_frame.setup_new_image(width, height, self.current_photo)
        self.main_pixmap.show()
        self.person_name.setText(unicode(self.current_person))
        self.main_image.fitInView(self.main_pixmap, QtCore.Qt.KeepAspectRatio)

        opinion = self.current_photo.opinion
        if opinion == 'ok':
            self.opinion_ok.setChecked(True)
        elif opinion == 'bad':
            self.opinion_bad.setChecked(True)
        elif opinion == 'unsure':
            self.opinion_unsure.setChecked(True)
        self.do_not_upload.setChecked(self.current_photo.block_upload)

        self.image_draw_needed = False

        # Now fire off an update event to get the preview drawn
        self.loading_now = False
        self.handle_crop()

    def _handle_opinion(self, state):
        if self.loading_now:
            return
        
        if self.current_photo is None:
            return

        self.current_photo.update_opinion(state)

    def handle_opinion_ok(self):
        self._handle_opinion('ok')
        
    def handle_opinion_bad(self):
        self._handle_opinion('bad')
        
    def handle_opinion_unsure(self):
        self._handle_opinion('unsure')

    def handle_do_not_upload(self, state):
        if self.loading_now:
            return
        
        if self.current_photo is None:
            return

        self.current_photo.update_block_upload(state)

    def handle_rotate(self, angle):
        if self.current_image is not None and not self.loading_now:
            self.image_draw_needed = True
            self.current_image.set_rotation(angle)
            self.current_photo.update_rotation(angle)

    def handle_brightness(self, brightness):
        if self.current_image is not None and not self.loading_now:
            self.image_draw_needed = True
            self.current_image.set_brightness(brightness / 127)
            self.current_photo.update_brightness(brightness / 127)

    def handle_contrast(self, contrast):
        if self.current_image is not None and not self.loading_now:
            self.image_draw_needed = True
            self.current_image.set_contrast(contrast / 127)
            self.current_photo.update_contrast(contrast / 127)

    def handle_gamma(self, gamma):
        if self.current_image is not None and not self.loading_now:
            self.image_draw_needed = True
            self.current_image.set_gamma(gamma / 10)
            self.current_photo.update_gamma(gamma / 10)

    def handle_crop(self):
        if self.loading_now:
            return
        
        if self.current_photo is None:
            return

        pixmap = self.main_pixmap.pixmap()
        pixmap = pixmap.copy(self.crop_frame.cropping_rect())

        orig_width, orig_height = self.current_image.orig_size()
        orig_size = orig_width * orig_height
        new_size = pixmap.width() * pixmap.height()

        pixmap = pixmap.scaled(102, 136, QtCore.Qt.KeepAspectRatio)
        self.preview_image.setPixmap(pixmap)

        size_change = new_size / orig_size
        self.percent_change.setText('%d%%' % int(100*size_change))
        if new_size < 5000:
            self.pixel_count.setText("<font color='red'><b>%d pixels</b></font>" % new_size)
        else:
            self.pixel_count.setText("%d pixels" % new_size)

    def set_ef_ops_enabled(self, enabled):
        self.action_fetch.setEnabled(enabled)
        self.action_upload.setEnabled(enabled)
        self.fetch_this_person.setEnabled(enabled)

    def handle_fetch_wizard(self):
        self.fetch_wizard = FetchWizard(self.username, self.password)
        self.fetch_wizard.start_fetch.connect(self.fetcher.start_fetch)
        self.fetch_wizard.rejected.connect(self.handle_fetch_rejected)
        self.fetch_wizard.start_fetch_reports.connect(self.reportsfetcher.start_fetch)
        self.reportsfetcher.completed.connect(self.fetch_wizard.reports_ready)
        self.fetch_wizard.show()
        self.set_ef_ops_enabled(False)

    def handle_fetch_person(self):
        if self.loading_now:
            return
        
        if self.current_person is None:
            return

        self.fetcher.start_fetch_person(self.current_person.id, self.username, self.password)
        self.set_ef_ops_enabled(False)

    def handle_fetch_rejected(self):
        self.set_ef_ops_enabled(True)

    def handle_fetch_completed(self, event):
        self.set_ef_ops_enabled(True)
        self.status_finished()
        if event > 0:
            QtCore.QSettings().setValue('last-fetched-%d' % event, QtCore.QDate.currentDate().toString(QtCore.Qt.ISODate))

    def handle_reportsfetch_error(self, err):
        print >>sys.stderr, err
        QtGui.QMessageBox.information(self, "Error during fetch of reports list", err)

    def handle_fetch_error(self, err):
        print >>sys.stderr, err
        self.status_finishing()
        QtGui.QMessageBox.information(self, "Error during fetch", err)
        self.set_ef_ops_enabled(True)
        self.status_finished()

    def handle_fetch_progress(self, text, cur, max):
        self.status_start(text, max)
        self.progress.setValue(cur)

    def handle_upload_wizard(self):
        # XXX: move to "change of current_person" functions
        self.upload_wizard.upload_photos_thisone.setEnabled(self.current_person is not None)
        self.upload_wizard.show()
        self.set_ef_ops_enabled(False)

    def handle_upload_rejected(self):
        self.set_ef_ops_enabled(True)

    def handle_upload(self):
        self.upload_wizard.restart()
        
        if self.upload_wizard.upload_photos_thisone.isChecked() and self.current_person is not None:
            upload_photos = {'mode': 'list', 'people': [self.current_person]}
        elif self.upload_wizard.upload_photos_bysize.isChecked():
            upload_photos = {'mode': 'percent', 'filter': int(self.upload_wizard.upload_photos_minsize.text())}
        elif self.upload_wizard.upload_photos_all.isChecked():
            upload_photos = {'mode': 'good'}
        else:
            return
        username = self.username
        password = self.password

        self.uploader.start_upload(upload_photos, username, password)

    def handle_upload_completed(self):
        self.set_ef_ops_enabled(True)
        self.status_finished()

    def handle_upload_error(self, err):
        print >>sys.stderr, err
        self.status_finishing()
        QtGui.QMessageBox.information(self, "Error during upload", err)
        self.set_ef_ops_enabled(True)
        self.status_finished()

    def handle_upload_progress(self, text, cur, max):
        self.status_start(text, max)
        self.progress.setValue(cur)

    def handle_db_created(self, obj):
        if isinstance(obj, Person):
            item = self.image_list_items[obj.id] = ImageListItem(self.photodownloader, self.list_photo_cache, obj)
            self.person_model.appendRow(item)

            if self.person_loaded and -1 == self.filter_police.findText(obj.police_status):
                self.filter_police.addItem(obj.police_status)
        elif isinstance(obj, Event):
            self.filter_event.addItem(obj.name, obj.id)
            default_event, ok = self.settings.value('filter-event', '0').toInt()
            if ok and obj.id == default_event:
                index = self.filter_event.findData(default_event)
                if index >= 0:
                    self.filter_event.setCurrentIndex(index)
        elif isinstance(obj, Registration):
            if self.registration_loaded and -1 == self.filter_category.findText(obj.attendee_type):
                self.filter_category.addItem(obj.attendee_type)            

    def handle_db_exception(self, e, msg):
        print >>sys.stderr, msg
        QtGui.QMessageBox.information(self, "Error while accessing database", msg)

    def handle_db_existing_done(self, table):
        if table == 'person':
            #print "Finished generating people"
            # Hook up the signals that we didn't want to fire while the database was loading (too many pointless repetitions)
            self.person_list.setModel(self.person_model_proxy)
            self.person_list.selectionModel().currentChanged.connect(self.handle_select)
            self.person_model.itemChanged.connect(self.handle_model_item_changed)

            for status in sorted(Person.statuses):
                self.filter_police.addItem(status)
            self.person_loaded = True

            self.main_stack.setCurrentIndex(1)
            if self.username == '':
                self.ef_username.setFocus()
            else:
                self.ef_password.setFocus()
        elif table == 'registration':
            for category in sorted(Registration.categories):
                self.filter_category.addItem(category)
            self.registration_loaded = True

    def handle_filter_event_changed(self, index):
        id = self.filter_event.itemData(index).toPyObject()
        self.person_model_proxy.set_event_id(id)
        QtCore.QSettings().setValue('filter-event', id)

    def handle_openeventsforce(self):
        if self.current_person is not None:
            QtGui.QDesktopServices.openUrl(QtCore.QUrl('https://www.eventsforce.net/libdems/backend/home/codEditMain.csp?codReadOnly=1&personID=%d&curPage=1' % self.current_person.id))

    def handle_chooseeditor(self):
        if self.chooseeditor.exec_():
            QtCore.QSettings().setValue('chooseeditor-state', self.chooseeditor.saveState())
            filenames = self.chooseeditor.selectedFiles()
            self.image_editor = str(filenames[0]) if filenames else None
            QtCore.QSettings().setValue('image-editor', self.image_editor)

    def handle_editimage(self):
        if self.current_person is None or self.current_photo is None:
            return

        path = self.current_photo.full_path()
        if not os.path.exists(path):
            return

        if not self.image_editor:
            self.handle_chooseeditor()

        if not self.image_editor:
            return

        f = open(path, 'rb')
        tf = tempfile.NamedTemporaryFile(prefix='%d_%s_' % (self.current_person.id, self.current_person.fullname), suffix='.jpg', delete=False)
        shutil.copyfileobj(f, tf)
        tf.close()

        proc = QtCore.QProcess(self)
        proc.setProcessChannelMode(QtCore.QProcess.ForwardedChannels)
        proc.start(self.image_editor, [tf.name])

        pid = proc.pid()
        self.procs[pid] = proc
        proc.finished.connect(lambda: self.handle_process_finished(pid))

    def handle_process_finished(self, pid):
        proc = self.procs.pop(pid, None)

    def handle_import_photo(self):
        if self.current_person is None:
            return

        self.action_importphoto.setEnabled(False)

        if not self.openimage.exec_():
            self.action_importphoto.setEnabled(True)
            return

        QtCore.QSettings().setValue('openimage-state', self.openimage.saveState())

        filenames = self.openimage.selectedFiles()
        filename = str(filenames[0])
        try:
            Image.open(filename).verify()
        except Exception, e:
            QtGui.QMessageBox.information(self, "Error loading image", str(e))
            return

        local_filename = stash_photo(filename)

        self.import_batch = Batch()
        self.import_fetchedphoto = FetchedPhoto(self.current_person, None, self.import_batch, local_filename=local_filename)
        self.import_batch.finished.connect(self.handle_import_finished)
        self.import_batch.finish()