Ejemplo n.º 1
0
class StreamProxy(QtCore.QObject):
    # only the GUI thread is allowed to write messages in the
    # LoggerWindow, so this class acts as a proxy, passing messages
    # over Qt signal/slot for thread safety
    flush_text = QtSignal()
    write_text = QtSignal(str)

    def write(self, msg):
        msg = msg.strip()
        if msg:
            self.write_text.emit(msg)

    def flush(self):
        self.flush_text.emit()
Ejemplo n.º 2
0
class UploaderSession(QtCore.QObject):
    connection_changed = QtSignal(bool)

    def __init__(self, *arg, **kwds):
        super(UploaderSession, self).__init__(*arg, **kwds)
        self.api = None
        # get api client id and secret
        for option in key_store.config.options(self.name):
            setattr(self, option, key_store.get(self.name, option))

    @QtSlot()
    @catch_all
    def log_out(self):
        keyring.delete_password('photini', self.name)
        self.close_connection()

    def close_connection(self):
        self.connection_changed.emit(False)
        if self.api:
            self.api.close()
            self.api = None

    def get_password(self):
        return keyring.get_password('photini', self.name)

    def set_password(self, password):
        keyring.set_password('photini', self.name, password)
Ejemplo n.º 3
0
class AuthServer(QtCore.QObject):
    finished = QtSignal()
    response = QtSignal(dict)

    @QtSlot()
    @catch_all
    def handle_requests(self):
        self.server.timeout = 10
        self.server.result = None
        # allow user 5 minutes to finish the process
        timeout = time.time() + 300
        while time.time() < timeout:
            self.server.handle_request()
            if self.server.result:
                self.response.emit(self.server.result)
                break
        self.server.server_close()
        self.finished.emit()
Ejemplo n.º 4
0
class GoogleUploadConfig(QtWidgets.QWidget):
    new_set = QtSignal()

    def __init__(self, *arg, **kw):
        super(GoogleUploadConfig, self).__init__(*arg, **kw)
        self.setLayout(QtWidgets.QGridLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        # create new set
        new_set_button = QtWidgets.QPushButton(
            translate('GooglePhotosTab', 'New album'))
        new_set_button.clicked.connect(self.new_set)
        self.layout().addWidget(new_set_button, 2, 1)
        # list of sets widget
        sets_group = QtWidgets.QGroupBox(
            translate('GooglePhotosTab', 'Add to albums'))
        sets_group.setLayout(QtWidgets.QVBoxLayout())
        scrollarea = QtWidgets.QScrollArea()
        scrollarea.setFrameStyle(QtWidgets.QFrame.NoFrame)
        scrollarea.setStyleSheet(
            "QScrollArea { background-color: transparent }")
        self.sets_widget = QtWidgets.QWidget()
        self.sets_widget.setLayout(QtWidgets.QVBoxLayout())
        self.sets_widget.layout().setSpacing(0)
        self.sets_widget.layout().setSizeConstraint(
            QtWidgets.QLayout.SetMinAndMaxSize)
        scrollarea.setWidget(self.sets_widget)
        self.sets_widget.setAutoFillBackground(False)
        sets_group.layout().addWidget(scrollarea)
        self.layout().addWidget(sets_group, 0, 2, 3, 1)
        self.layout().setColumnStretch(2, 1)

    def clear_sets(self):
        for child in self.sets_widget.children():
            if child.isWidgetType():
                self.sets_widget.layout().removeWidget(child)
                child.setParent(None)

    def checked_albums(self):
        result = []
        for child in self.sets_widget.children():
            if child.isWidgetType() and child.isChecked():
                result.append(child.property('id'))
        return result

    def add_album(self, album, index=-1):
        widget = QtWidgets.QCheckBox(album['title'].replace('&', '&&'))
        widget.setProperty('id', album['id'])
        widget.setEnabled(album['isWriteable'])
        if index >= 0:
            self.sets_widget.layout().insertWidget(index, widget)
        else:
            self.sets_widget.layout().addWidget(widget)
        return widget
Ejemplo n.º 5
0
class DateLink(QtWidgets.QCheckBox):
    new_link = QtSignal(str)

    def __init__(self, name, *arg, **kw):
        super(DateLink, self).__init__(*arg, **kw)
        self.name = name
        self.clicked.connect(self._clicked)

    @QtSlot()
    @catch_all
    def _clicked(self):
        self.new_link.emit(self.name)
Ejemplo n.º 6
0
class DateAndTimeWidget(QtWidgets.QGridLayout):
    new_value = QtSignal(str, dict)

    def __init__(self, name, *arg, **kw):
        super(DateAndTimeWidget, self).__init__(*arg, **kw)
        self.name = name
        self.setVerticalSpacing(0)
        self.setColumnStretch(3, 1)
        self.members = {}
        # date & time
        self.members['datetime'] = DateTimeEdit()
        self.addWidget(self.members['datetime'], 0, 0, 1, 2)
        # time zone
        self.members['tz_offset'] = TimeZoneWidget()
        self.addWidget(self.members['tz_offset'], 0, 2)
        # precision
        self.addWidget(
            QtWidgets.QLabel(translate('TechnicalTab', 'Precision:')), 1, 0)
        self.members['precision'] = PrecisionSlider(Qt.Horizontal)
        self.members['precision'].setRange(1, 6)
        self.members['precision'].setValue(6)
        self.members['precision'].setPageStep(1)
        self.addWidget(self.members['precision'], 1, 1)
        # connections
        self.members['precision'].value_changed.connect(
            self.members['datetime'].set_precision)
        self.members['datetime'].editingFinished.connect(self.editing_finished)
        self.members['tz_offset'].editingFinished.connect(self.editing_finished)
        self.members['precision'].editing_finished.connect(self.editing_finished)

    def set_enabled(self, enabled):
        for widget in self.members.values():
            widget.setEnabled(enabled)

    def get_value(self):
        new_value = {}
        for key in self.members:
            if self.members[key].is_multiple():
                continue
            new_value[key] = self.members[key].get_value()
            if key == 'datetime' and new_value[key]:
                if using_pyside:
                    new_value[key] = new_value[key].toPython()
                else:
                    new_value[key] = new_value[key].toPyDateTime()
        return new_value

    @QtSlot()
    @catch_all
    def editing_finished(self):
        self.new_value.emit(self.name, self.get_value())
Ejemplo n.º 7
0
class UploaderSession(QtCore.QObject):
    connection_changed = QtSignal(bool)

    @QtSlot()
    @catch_all
    def log_out(self):
        keyring.delete_password('photini', self.name)
        self.close_connection()

    def get_password(self):
        return keyring.get_password('photini', self.name)

    def set_password(self, password):
        keyring.set_password('photini', self.name, password)
Ejemplo n.º 8
0
class LatLongDisplay(SingleLineEdit):
    changed = QtSignal()

    def __init__(self, image_list, *args, **kwds):
        super(LatLongDisplay, self).__init__(*args, **kwds)
        self.image_list = image_list
        self.label = QtWidgets.QLabel(translate('MapTabsAll', 'Lat, long'))
        self.label.setAlignment(Qt.AlignRight)
        self.setFixedWidth(width_for_text(self, '8' * 23))
        self.setEnabled(False)
        self.editingFinished.connect(self.editing_finished)

    @QtSlot()
    @catch_all
    def editing_finished(self):
        selected_images = self.image_list.get_selected_images()
        new_value = self.get_value().strip() or None
        if new_value:
            try:
                new_value = list(map(float, new_value.split(',')))
            except Exception:
                # user typed in an invalid value
                self.update_display(selected_images)
                return
        for image in selected_images:
            image.metadata.latlong = new_value
        self.update_display(selected_images)
        self.changed.emit()

    def update_display(self, selected_images=None):
        if selected_images is None:
            selected_images = self.image_list.get_selected_images()
        if not selected_images:
            self.set_value(None)
            self.setEnabled(False)
            return
        values = []
        for image in selected_images:
            value = image.metadata.latlong
            if value not in values:
                values.append(value)
        if len(values) > 1:
            self.set_multiple(choices=filter(None, values))
        else:
            self.set_value(values[0])
        self.setEnabled(True)
Ejemplo n.º 9
0
class LocationInfo(QtWidgets.QWidget):
    new_value = QtSignal(object, dict)

    def __init__(self, *args, **kw):
        super(LocationInfo, self).__init__(*args, **kw)
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        layout.setContentsMargins(0, 0, 0, 0)
        self.members = {}
        for key in ('sublocation', 'city', 'province_state', 'country_name',
                    'country_code', 'world_region'):
            self.members[key] = SingleLineEdit()
            self.members[key].editingFinished.connect(self.editing_finished)
        self.members['country_code'].setMaximumWidth(40)
        for j, text in enumerate((
                translate('AddressTab', 'Street'),
                translate('AddressTab', 'City'),
                translate('AddressTab', 'Province'),
                translate('AddressTab', 'Country'),
                translate('AddressTab', 'Region'),
        )):
            label = QtWidgets.QLabel(text)
            label.setAlignment(Qt.AlignRight)
            layout.addWidget(label, j, 0)
        layout.addWidget(self.members['sublocation'], 0, 1, 1, 2)
        layout.addWidget(self.members['city'], 1, 1, 1, 2)
        layout.addWidget(self.members['province_state'], 2, 1, 1, 2)
        layout.addWidget(self.members['country_name'], 3, 1)
        layout.addWidget(self.members['country_code'], 3, 2)
        layout.addWidget(self.members['world_region'], 4, 1, 1, 2)
        layout.setRowStretch(5, 1)

    def get_value(self):
        new_value = {}
        for key in self.members:
            if self.members[key].is_multiple():
                continue
            new_value[key] = self.members[key].get_value().strip() or None
        return new_value

    @QtSlot()
    @catch_all
    def editing_finished(self):
        self.new_value.emit(self, self.get_value())
Ejemplo n.º 10
0
class NameMangler(QtCore.QObject):
    number_parser = re.compile(r'(\d+)')
    new_example = QtSignal(str)

    def __init__(self, parent=None):
        super(NameMangler, self).__init__(parent)
        self.example = None
        self.format_string = None

    @QtSlot(str)
    @catch_all
    def new_format(self, format_string):
        self.format_string = format_string
        self.refresh_example()

    def set_example(self, example):
        self.example = example
        self.refresh_example()

    def refresh_example(self):
        if self.format_string and self.example:
            self.new_example.emit(self.transform(self.example))

    def transform(self, file_data):
        name = file_data['name']
        subst = {'name': name}
        numbers = self.number_parser.findall(name)
        if numbers:
            subst['number'] = numbers[-1]
        else:
            subst['number'] = ''
        subst['root'], subst['ext'] = os.path.splitext(name)
        subst['camera'] = file_data['camera'] or 'unknown_camera'
        subst['camera'] = subst['camera'].replace(' ', '_')
        # process {...} parts first
        try:
            result = self.format_string.format(**subst)
        except (KeyError, ValueError):
            result = self.format_string
        # then do timestamp
        return file_data['timestamp'].strftime(result)
Ejemplo n.º 11
0
class MapWebView(QWebView):
    drop_text = QtSignal(int, int, str)

    @catch_all
    def dragEnterEvent(self, event):
        if not event.mimeData().hasFormat(DRAG_MIMETYPE):
            return super(MapWebView, self).dragEnterEvent(event)
        event.acceptProposedAction()

    @catch_all
    def dragMoveEvent(self, event):
        if not event.mimeData().hasFormat(DRAG_MIMETYPE):
            return super(MapWebView, self).dragMoveEvent(event)

    @catch_all
    def dropEvent(self, event):
        if not event.mimeData().hasFormat(DRAG_MIMETYPE):
            return super(MapWebView, self).dropEvent(event)
        text = event.mimeData().data(DRAG_MIMETYPE).data().decode('utf-8')
        if text:
            self.drop_text.emit(event.pos().x(), event.pos().y(), text)
Ejemplo n.º 12
0
class PrecisionSlider(Slider):
    value_changed = QtSignal(int)

    def __init__(self, *arg, **kw):
        super(PrecisionSlider, self).__init__(*arg, **kw)
        self.valueChanged.connect(self._value_changed)

    def _value_changed(self, value):
        if value >= 4:
            value += 1
        self.value_changed.emit(value)

    def get_value(self):
        value = super(PrecisionSlider, self).get_value()
        if value >= 4:
            value += 1
        return value

    def set_value(self, value):
        if value is not None and value >= 5:
            value -= 1
        super(PrecisionSlider, self).set_value(value)
Ejemplo n.º 13
0
class ServerSocket(QtCore.QObject):
    new_files = QtSignal(list)

    def __init__(self, socket, *arg, **kw):
        super(ServerSocket, self).__init__(*arg, **kw)
        self.socket = socket
        self.data = b''
        self.socket.setParent(self)
        self.socket.readyRead.connect(self.read_data)
        self.socket.disconnected.connect(self.deleteLater)

    @QtSlot()
    @catch_all
    def read_data(self):
        file_list = []
        while self.socket.bytesAvailable():
            self.data += self.socket.readAll().data()
            while b'\n' in self.data:
                line, sep, self.data = self.data.partition(b'\n')
                string = line.decode('utf-8')
                file_list.append(string)
        if file_list:
            self.new_files.emit(file_list)
Ejemplo n.º 14
0
class InstanceServer(QtNetwork.QTcpServer):
    new_files = QtSignal(list)

    def __init__(self, *arg, **kw):
        super(InstanceServer, self).__init__(*arg, **kw)
        config = BaseConfigStore('instance')
        self.newConnection.connect(self.new_connection)
        if not self.listen(QtNetwork.QHostAddress.LocalHost):
            logger.error('Failed to start instance server:',
                         self.errorString())
            return
        config.set('server', 'port', self.serverPort())
        config.save()

    @QtSlot()
    @catch_all
    def new_connection(self):
        window = self.parent().window()
        window.raise_()
        window.activateWindow()
        while self.hasPendingConnections():
            socket = self.nextPendingConnection()
            socket = ServerSocket(socket, parent=self)
            socket.new_files.connect(self.new_files)
Ejemplo n.º 15
0
class QTabBar(QtWidgets.QTabBar):
    context_menu = QtSignal(QtGui.QContextMenuEvent)

    @catch_all
    def contextMenuEvent(self, event):
        self.context_menu.emit(event)
Ejemplo n.º 16
0
class OffsetWidget(QtWidgets.QWidget):
    apply_offset = QtSignal(timedelta, object)

    def __init__(self, *arg, **kw):
        super(OffsetWidget, self).__init__(*arg, **kw)
        self.config_store = QtWidgets.QApplication.instance().config_store
        self.setLayout(QtWidgets.QHBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        spacing = self.layout().spacing()
        self.layout().setSpacing(0)
        # offset value
        self.offset = QtWidgets.QTimeEdit()
        self.offset.setDisplayFormat("'h:'hh 'm:'mm 's:'ss")
        self.layout().addWidget(self.offset)
        self.layout().addSpacing(spacing)
        # time zone
        self.time_zone = TimeZoneWidget()
        self.time_zone.set_value(None)
        self.layout().addWidget(self.time_zone)
        self.layout().addSpacing(spacing)
        # add offset button
        add_button = QtWidgets.QPushButton(chr(0x002b))
        add_button.setStyleSheet('QPushButton {padding: 0px}')
        set_symbol_font(add_button)
        scale_font(add_button, 170)
        add_button.setFixedWidth(self.offset.sizeHint().height())
        add_button.setFixedHeight(self.offset.sizeHint().height())
        add_button.clicked.connect(self.add)
        self.layout().addWidget(add_button)
        # subtract offset button
        sub_button = QtWidgets.QPushButton(chr(0x2212))
        sub_button.setStyleSheet('QPushButton {padding: 0px}')
        set_symbol_font(sub_button)
        scale_font(sub_button, 170)
        sub_button.setFixedWidth(self.offset.sizeHint().height())
        sub_button.setFixedHeight(self.offset.sizeHint().height())
        sub_button.clicked.connect(self.sub)
        self.layout().addWidget(sub_button)
        self.layout().addStretch(1)
        # restore stored values
        value = self.config_store.get('technical', 'offset')
        if value:
            self.offset.setTime(QtCore.QTime(*value[0:3]))
            self.time_zone.set_value(value[3])
        # connections
        self.offset.editingFinished.connect(self.new_value)
        self.time_zone.editingFinished.connect(self.new_value)

    @catch_all
    def showEvent(self, event):
        super(OffsetWidget, self).showEvent(event)
        # On some Windows versions the initial sizeHint calculation is
        # wrong. Redoing it after the widget becomes visible gets a
        # better result. Calling setSpecialValueText is also required.
        self.offset.setSpecialValueText('')
        self.offset.updateGeometry()
        self.time_zone.setSpecialValueText(' ')
        self.time_zone.updateGeometry()

    @QtSlot()
    @catch_all
    def new_value(self):
        value = self.offset.time()
        value = (value.hour(), value.minute(), value.second(),
                 self.time_zone.get_value())
        self.config_store.set('technical', 'offset', value)

    @QtSlot()
    @catch_all
    def add(self):
        self.do_inc(False)

    @QtSlot()
    @catch_all
    def sub(self):
        self.do_inc(True)

    def do_inc(self, negative):
        value = self.offset.time()
        offset = timedelta(
            hours=value.hour(), minutes=value.minute(), seconds=value.second())
        tz_offset = self.time_zone.get_value()
        if negative:
            if tz_offset is not None:
                tz_offset = -tz_offset
            offset = -offset
        self.apply_offset.emit(offset, tz_offset)
Ejemplo n.º 17
0
class PhotiniUploader(QtWidgets.QWidget):
    abort_upload = QtSignal(bool)

    def __init__(self, upload_config_widget, image_list, *arg, **kw):
        super(PhotiniUploader, self).__init__(*arg, **kw)
        self.app = QtWidgets.QApplication.instance()
        self.app.aboutToQuit.connect(self.shutdown)
        logger.debug('using %s', keyring.get_keyring().__module__)
        self.image_list = image_list
        self.setLayout(QtWidgets.QGridLayout())
        self.session = self.session_factory()
        self.session.connection_changed.connect(self.connection_changed)
        self.upload_worker = None
        # user details
        self.user = {}
        user_group = QtWidgets.QGroupBox(translate('UploaderTabsAll', 'User'))
        user_group.setLayout(QtWidgets.QVBoxLayout())
        self.user_photo = QtWidgets.QLabel()
        self.user_photo.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
        user_group.layout().addWidget(self.user_photo)
        self.user_name = QtWidgets.QLabel()
        self.user_name.setWordWrap(True)
        self.user_name.setFixedWidth(80)
        user_group.layout().addWidget(self.user_name)
        user_group.layout().addStretch(1)
        self.layout().addWidget(user_group, 0, 0, 1, 2)
        # connect / disconnect button
        self.user_connect = StartStopButton(
            translate('UploaderTabsAll', 'Log in'),
            translate('UploaderTabsAll', 'Log out'))
        self.user_connect.click_start.connect(self.log_in)
        self.user_connect.click_stop.connect(self.session.log_out)
        self.layout().addWidget(self.user_connect, 1, 0, 1, 2)
        # 'service' specific widget
        self.layout().addWidget(upload_config_widget, 0, 2, 2, 2)
        # upload button
        self.upload_button = StartStopButton(
            translate('UploaderTabsAll', 'Start upload'),
            translate('UploaderTabsAll', 'Stop upload'))
        self.upload_button.setEnabled(False)
        self.upload_button.click_start.connect(self.start_upload)
        self.upload_button.click_stop.connect(self.stop_upload)
        self.layout().addWidget(self.upload_button, 2, 3)
        # progress bar
        self.layout().addWidget(
            QtWidgets.QLabel(translate('UploaderTabsAll', 'Progress')), 2, 0)
        self.total_progress = QtWidgets.QProgressBar()
        self.layout().addWidget(self.total_progress, 2, 1, 1, 2)
        # adjust spacing
        self.layout().setColumnStretch(2, 1)
        self.layout().setRowStretch(0, 1)
        # initialise as not connected
        self.connection_changed(False)

    def tr(self, *arg, **kw):
        return QtCore.QCoreApplication.translate('UploaderTabsAll', *arg, **kw)

    @QtSlot()
    @catch_all
    def shutdown(self):
        self.session.close_connection()

    @QtSlot(bool)
    @catch_all
    def connection_changed(self, connected):
        if connected:
            with Busy():
                self.show_user(*self.session.get_user())
                self.show_album_list(self.session.get_albums())
        else:
            self.show_user(None, None)
            self.show_album_list([])
        self.user_connect.set_checked(connected)
        self.upload_config.setEnabled(connected and not self.upload_worker)
        self.user_connect.setEnabled(not self.upload_worker)
        self.enable_upload_button()

    def refresh(self):
        if not self.user_connect.is_checked():
            self.log_in(do_auth=False)
        self.enable_upload_button()

    def do_not_close(self):
        if not self.upload_worker:
            return False
        dialog = QtWidgets.QMessageBox(parent=self)
        dialog.setWindowTitle(translate(
            'UploaderTabsAll', 'Photini: upload in progress'))
        dialog.setText(translate(
            'UploaderTabsAll',
            '<h3>Upload to {} has not finished.</h3>').format(self.service_name))
        dialog.setInformativeText(
            translate('UploaderTabsAll', 'Closing now will terminate the upload.'))
        dialog.setIcon(QtWidgets.QMessageBox.Warning)
        dialog.setStandardButtons(
            QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel)
        dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel)
        result = dialog.exec_()
        return result == QtWidgets.QMessageBox.Cancel

    def show_user(self, name, picture):
        if name:
            self.user_name.setText(translate(
                'UploaderTabsAll',
                'Logged in as {0} on {1}').format(name, self.service_name))
        else:
            self.user_name.setText(translate(
                'UploaderTabsAll',
                'Not logged in to {}').format(self.service_name))
        pixmap = QtGui.QPixmap()
        if picture:
            pixmap.loadFromData(picture)
        self.user_photo.setPixmap(pixmap)

    def get_temp_filename(self, image, ext='.jpg'):
        temp_dir = appdirs.user_cache_dir('photini')
        if not os.path.isdir(temp_dir):
            os.makedirs(temp_dir)
        return os.path.join(temp_dir, os.path.basename(image.path) + ext)

    def copy_metadata(self, image, path):
        # copy metadata
        md = Metadata.clone(path, image.metadata)
        # save metedata, forcing IPTC creation
        md.dirty = True
        md.save(if_mode=True, sc_mode='none', force_iptc=True)

    def convert_to_jpeg(self, image):
        im = QtGui.QImage(image.path)
        path = self.get_temp_filename(image)
        im.save(path, format='jpeg', quality=95)
        self.copy_metadata(image, path)
        return path

    def copy_file_and_metadata(self, image):
        path = self.get_temp_filename(image, ext='')
        shutil.copyfile(image.path, path)
        self.copy_metadata(image, path)
        return path

    def is_convertible(self, image):
        if not image.file_type.startswith('image'):
            # can only convert images
            return False
        return QtGui.QImageReader(image.path).canRead()

    def get_conversion_function(self, image, params):
        if image.file_type in self.image_types['accepted']:
            if image.file_type.startswith('video'):
                # don't try to write metadata to videos
                return None
            if image.metadata.find_sidecar() or not image.metadata.iptc_in_file:
                # need to create file without sidecar and with IPTC
                return self.copy_file_and_metadata
            return None
        if not self.is_convertible(image):
            msg = translate(
                'UploaderTabsAll',
                'File "{0}" is of type "{1}", which {2} does not' +
                ' accept and Photini cannot convert.')
            buttons = QtWidgets.QMessageBox.Ignore
        elif (self.image_types['rejected'] == '*' or
              image.file_type in self.image_types['rejected']):
            msg = translate(
                'UploaderTabsAll',
                'File "{0}" is of type "{1}", which {2} does not' +
                ' accept. Would you like to convert it to JPEG?')
            buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Ignore
        else:
            msg = translate(
                'UploaderTabsAll',
                'File "{0}" is of type "{1}", which {2} may not' +
                ' handle correctly. Would you like to convert it to JPEG?')
            buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
        dialog = QtWidgets.QMessageBox(parent=self)
        dialog.setWindowTitle(
            translate('UploaderTabsAll', 'Photini: incompatible type'))
        dialog.setText(
            translate('UploaderTabsAll', '<h3>Incompatible image type.</h3>'))
        dialog.setInformativeText(msg.format(os.path.basename(image.path),
                                             image.file_type, self.service_name))
        dialog.setIcon(QtWidgets.QMessageBox.Warning)
        dialog.setStandardButtons(buttons)
        dialog.setDefaultButton(QtWidgets.QMessageBox.Yes)
        result = dialog.exec_()
        if result == QtWidgets.QMessageBox.Ignore:
            return 'omit'
        if result == QtWidgets.QMessageBox.Yes:
            return self.convert_to_jpeg
        return None

    @QtSlot()
    @catch_all
    def stop_upload(self):
        self.abort_upload.emit(False)

    @QtSlot()
    @catch_all
    def start_upload(self):
        if not self.image_list.unsaved_files_dialog(with_discard=False):
            return
        # make list of items to upload
        upload_list = []
        for image in self.image_list.get_selected_images():
            params = self.get_upload_params(image)
            if not params:
                continue
            convert = self.get_conversion_function(image, params)
            if convert == 'omit':
                continue
            upload_list.append((image, convert, params))
        if not upload_list:
            self.upload_button.setChecked(False)
            return
        self.upload_button.set_checked(True)
        self.upload_config.setEnabled(False)
        self.user_connect.setEnabled(False)
        # do uploading in separate thread, so GUI can continue
        self.upload_worker = UploadWorker(self.session_factory, upload_list)
        thread = QtCore.QThread(self)
        self.upload_worker.moveToThread(thread)
        self.upload_worker.upload_error.connect(
            self.upload_error, Qt.BlockingQueuedConnection)
        self.abort_upload.connect(
            self.upload_worker.abort_upload, Qt.DirectConnection)
        self.upload_worker.upload_progress.connect(self.upload_progress)
        thread.started.connect(self.upload_worker.start)
        self.upload_worker.finished.connect(self.uploader_finished)
        self.upload_worker.finished.connect(thread.quit)
        self.upload_worker.finished.connect(self.upload_worker.deleteLater)
        thread.finished.connect(thread.deleteLater)
        thread.start()

    @QtSlot(float, str)
    @catch_all
    def upload_progress(self, value, format_):
        self.total_progress.setValue(value)
        if format_:
            self.total_progress.setFormat(format_)

    @QtSlot(str, str)
    @catch_all
    def upload_error(self, name, error):
        dialog = QtWidgets.QMessageBox(self)
        dialog.setWindowTitle(translate(
            'UploaderTabsAll', 'Photini: upload error'))
        dialog.setText(translate(
            'UploaderTabsAll', '<h3>File "{}" upload failed.</h3>').format(
                name))
        dialog.setInformativeText(error)
        dialog.setIcon(QtWidgets.QMessageBox.Warning)
        dialog.setStandardButtons(QtWidgets.QMessageBox.Abort |
                                  QtWidgets.QMessageBox.Retry)
        dialog.setDefaultButton(QtWidgets.QMessageBox.Retry)
        self.abort_upload.emit(dialog.exec_() == QtWidgets.QMessageBox.Retry)

    @QtSlot()
    @catch_all
    def uploader_finished(self):
        self.upload_button.set_checked(False)
        self.upload_config.setEnabled(True)
        self.user_connect.setEnabled(True)
        self.upload_worker = None
        self.enable_upload_button()

    @QtSlot()
    @catch_all
    def log_in(self, do_auth=True):
        with DisableWidget(self.user_connect):
            with Busy():
                connect = self.session.open_connection()
            if connect is None:
                # can't reach server
                return
            if do_auth and not connect:
                self.authorise()

    def authorise(self):
        with Busy():
            # do full authentication procedure
            http_server = HTTPServer(('127.0.0.1', 0), AuthRequestHandler)
            redirect_uri = 'http://127.0.0.1:' + str(http_server.server_port)
            auth_url = self.session.get_auth_url(redirect_uri)
            if not auth_url:
                logger.error('Failed to get auth URL')
                http_server.server_close()
                return
            server = AuthServer()
            thread = QtCore.QThread(self)
            server.moveToThread(thread)
            server.server = http_server
            server.response.connect(self.auth_response)
            thread.started.connect(server.handle_requests)
            server.finished.connect(thread.quit)
            server.finished.connect(server.deleteLater)
            thread.finished.connect(thread.deleteLater)
            thread.start()
            if QtGui.QDesktopServices.openUrl(QtCore.QUrl(auth_url)):
                return
            logger.error('Failed to open web browser')

    @QtSlot(dict)
    @catch_all
    def auth_response(self, result):
        with Busy():
            self.session.get_access_token(result)

    @QtSlot(list)
    @catch_all
    def new_selection(self, selection):
        self.enable_upload_button(selection=selection)

    def enable_upload_button(self, selection=None):
        if self.upload_button.is_checked():
            # can always cancel upload in progress
            self.upload_button.setEnabled(True)
            return
        if not self.user_connect.is_checked():
            # can't upload if not logged in
            self.upload_button.setEnabled(False)
            return
        if selection is None:
            selection = self.image_list.get_selected_images()
        self.upload_button.setEnabled(len(selection) > 0)
Ejemplo n.º 18
0
class AugmentSpinBox(object):
    new_value = QtSignal(object)

    def init_augment(self):
        self.set_value(None)
        self.editingFinished.connect(self.editing_finished)

    class ContextAction(QtGui2.QAction):
        def __init__(self, value, *arg, **kw):
            super(AugmentSpinBox.ContextAction, self).__init__(*arg, **kw)
            self.setData(value)
            self.triggered.connect(self.set_value)

        @QtSlot()
        @catch_all
        def set_value(self):
            self.parent().setValue(self.data())

    def context_menu_event(self):
        if self.specialValueText() and self.choices:
            QtCore.QTimer.singleShot(0, self.extend_context_menu)

    @QtSlot()
    @catch_all
    def extend_context_menu(self):
        menu = self.findChild(QtWidgets.QMenu)
        if not menu:
            return
        sep = menu.insertSeparator(menu.actions()[0])
        for suggestion in self.choices:
            menu.insertAction(sep, self.ContextAction(
                suggestion, text=self.textFromValue(suggestion), parent=self))

    def clear_special_value(self):
        if self.specialValueText():
            self.set_value(self.default_value)
            self.selectAll()

    @QtSlot()
    @catch_all
    def editing_finished(self):
        if self.is_multiple():
            return
        self.get_value(emit=True)

    def get_value(self, emit=False):
        value = self.value()
        if value == self.minimum() and self.specialValueText():
            value = None
        if emit:
            self.new_value.emit(value)
        return value

    def set_value(self, value):
        if value is None:
            self.setValue(self.minimum())
            self.setSpecialValueText(' ')
        else:
            self.setSpecialValueText('')
            self.setValue(value)

    def set_multiple(self, choices=[]):
        self.choices = list(filter(None, choices))
        self.setValue(self.minimum())
        self.setSpecialValueText(self.multiple)

    def is_multiple(self):
        return (self.value() == self.minimum()
                and self.specialValueText() == self.multiple)
Ejemplo n.º 19
0
class DropdownEdit(ComboBox):
    extend_list = QtSignal()
    new_value = QtSignal(object)

    def __init__(self, extendable=False, **kw):
        super(DropdownEdit, self).__init__(**kw)
        if extendable:
            self.setContextMenuPolicy(Qt.CustomContextMenu)
            self.customContextMenuRequested.connect(self.remove_from_list)
            self.addItem(
                translate('TechnicalTab', '<new>'), self.extend_list.emit)
        self.addItem('', None)
        self.first_value_idx = self.count()
        self.addItem(multiple_values(), '<multiple>')
        self.setItemData(
            self.count() - 1, self.itemData(self.count() - 1), Qt.UserRole - 1)
        self.currentIndexChanged.connect(self.current_index_changed)

    @QtSlot(QtCore.QPoint)
    @catch_all
    def remove_from_list(self, pos):
        current_value = self.itemData(self.currentIndex())
        menu = QtWidgets.QMenu()
        for name, value in self.get_items():
            if value == current_value:
                continue
            action = QtGui2.QAction(
                translate('TechnicalTab', 'Remove "{}"').format(name),
                parent=self)
            action.setData(value)
            menu.addAction(action)
        if menu.isEmpty():
            return
        action = menu.exec_(self.mapToGlobal(pos))
        if not action:
            return
        self.remove_item(action.data())

    @QtSlot(int)
    @catch_all
    def current_index_changed(self, idx):
        value = self.itemData(idx)
        if callable(value):
            (value)()
        else:
            self.new_value.emit(value)

    def add_item(self, text, value, ordered=True):
        blocked = self.blockSignals(True)
        position = self.count() - 1
        if ordered:
            for n in range(self.first_value_idx, self.count() - 1):
                if self.itemText(n).lower() > text.lower():
                    position = n
                    break
        self.insertItem(position, text, value)
        self.set_dropdown_width()
        self.blockSignals(blocked)

    def remove_item(self, value):
        blocked = self.blockSignals(True)
        self.removeItem(self.find_data(value))
        self.set_dropdown_width()
        self.blockSignals(blocked)

    def known_value(self, value):
        if not value:
            return True
        return self.find_data(value) >= 0

    def set_value(self, value):
        blocked = self.blockSignals(True)
        self.setCurrentIndex(self.find_data(value))
        self.blockSignals(blocked)

    def find_data(self, value):
        # Qt's findData only works for simple types
        for n in range(self.count()):
            if self.itemData(n) == value:
                return n
        return -1

    def get_items(self):
        for n in range(self.first_value_idx, self.count() - 1):
            yield self.itemText(n), self.itemData(n)

    def set_multiple(self):
        blocked = self.blockSignals(True)
        self.setCurrentIndex(self.count() - 1)
        self.blockSignals(blocked)
Ejemplo n.º 20
0
class UploadWorker(QtCore.QObject):
    finished = QtSignal()
    upload_error = QtSignal(str, str)
    upload_progress = QtSignal(float, str)

    def __init__(self, session_factory, upload_list, *args, **kwds):
        super(UploadWorker, self).__init__(*args, **kwds)
        self.session_factory = session_factory
        self.upload_list = upload_list
        self.fileobj = None

    @QtSlot()
    @catch_all
    def start(self):
        session = self.session_factory()
        session.open_connection()
        upload_count = 0
        while upload_count < len(self.upload_list):
            image, convert, params = self.upload_list[upload_count]
            name = os.path.basename(image.path)
            self.upload_progress.emit(0.0, '{} ({}/{}) %p%'.format(
                name, 1 + upload_count, len(self.upload_list)))
            if convert:
                path = convert(image)
            else:
                path = image.path
            with open(path, 'rb') as f:
                self.fileobj = FileObjWithCallback(f, self.progress)
                try:
                    error = session.do_upload(
                        self.fileobj, imghdr.what(path), image, params)
                except UploadAborted:
                    break
                except Exception as ex:
                    error = str(ex)
                self.fileobj = None
            if convert:
                os.unlink(path)
            if error:
                self.retry = None
                self.upload_error.emit(name, error)
                # wait for response from user dialog
                while self.retry is None:
                    QtWidgets.QApplication.processEvents()
                if not self.retry:
                    break
            else:
                upload_count += 1
        self.upload_progress.emit(0.0, '%p%')
        session.close_connection()
        self.finished.emit()

    def progress(self, value):
        self.upload_progress.emit(value, '')

    @QtSlot(bool)
    @catch_all
    def abort_upload(self, retry):
        self.retry = retry
        if self.fileobj:
            # brutal way to interrupt an upload
            self.fileobj.abort()
Ejemplo n.º 21
0
class FlickrUploadConfig(QtWidgets.QWidget):
    new_set = QtSignal()
    sync_metadata = QtSignal()

    def __init__(self, *arg, **kw):
        super(FlickrUploadConfig, self).__init__(*arg, **kw)
        self.setLayout(QtWidgets.QGridLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        # privacy settings
        self.privacy = {}
        privacy_group = QtWidgets.QGroupBox(
            translate('FlickrTab', 'Who can see the photos?'))
        privacy_group.setLayout(QtWidgets.QVBoxLayout())
        self.privacy['private'] = QtWidgets.QRadioButton(
            translate('FlickrTab', 'Only you'))
        privacy_group.layout().addWidget(self.privacy['private'])
        ff_group = QtWidgets.QGroupBox()
        ff_group.setFlat(True)
        ff_group.setLayout(QtWidgets.QVBoxLayout())
        ff_group.layout().setContentsMargins(10, 0, 0, 0)
        self.privacy['friends'] = QtWidgets.QCheckBox(
            translate('FlickrTab', 'Your friends'))
        ff_group.layout().addWidget(self.privacy['friends'])
        self.privacy['family'] = QtWidgets.QCheckBox(
            translate('FlickrTab', 'Your family'))
        ff_group.layout().addWidget(self.privacy['family'])
        privacy_group.layout().addWidget(ff_group)
        self.privacy['public'] = QtWidgets.QRadioButton(
            translate('FlickrTab', 'Anyone'))
        self.privacy['public'].toggled.connect(self.enable_ff)
        self.privacy['public'].setChecked(True)
        privacy_group.layout().addWidget(self.privacy['public'])
        self.hidden = QtWidgets.QCheckBox(
            translate('FlickrTab', 'Hidden from search'))
        privacy_group.layout().addWidget(self.hidden)
        privacy_group.layout().addStretch(1)
        self.layout().addWidget(privacy_group, 0, 0, 3, 1)
        # content type
        self.content_type = {}
        content_group = QtWidgets.QGroupBox(
            translate('FlickrTab', 'Content type'))
        content_group.setLayout(QtWidgets.QVBoxLayout())
        self.content_type['photo'] = QtWidgets.QRadioButton(
            translate('FlickrTab', 'Photo'))
        self.content_type['photo'].setChecked(True)
        content_group.layout().addWidget(self.content_type['photo'])
        self.content_type['screenshot'] = QtWidgets.QRadioButton(
            translate('FlickrTab', 'Screenshot'))
        content_group.layout().addWidget(self.content_type['screenshot'])
        self.content_type['other'] = QtWidgets.QRadioButton(
            translate('FlickrTab', 'Art/Illustration'))
        content_group.layout().addWidget(self.content_type['other'])
        content_group.layout().addStretch(1)
        self.layout().addWidget(content_group, 0, 1)
        # synchronise metadata
        self.sync_button = QtWidgets.QPushButton(
            translate('FlickrTab', 'Synchronise'))
        self.sync_button.clicked.connect(self.sync_metadata)
        self.layout().addWidget(self.sync_button, 1, 1)
        # create new set
        new_set_button = QtWidgets.QPushButton(
            translate('FlickrTab', 'New album'))
        new_set_button.clicked.connect(self.new_set)
        self.layout().addWidget(new_set_button, 2, 1)
        # list of sets widget
        sets_group = QtWidgets.QGroupBox(
            translate('FlickrTab', 'Add to albums'))
        sets_group.setLayout(QtWidgets.QVBoxLayout())
        scrollarea = QtWidgets.QScrollArea()
        scrollarea.setFrameStyle(QtWidgets.QFrame.NoFrame)
        scrollarea.setStyleSheet("QScrollArea { background-color: transparent }")
        self.sets_widget = QtWidgets.QWidget()
        self.sets_widget.setLayout(QtWidgets.QVBoxLayout())
        self.sets_widget.layout().setSpacing(0)
        self.sets_widget.layout().setSizeConstraint(
            QtWidgets.QLayout.SetMinAndMaxSize)
        scrollarea.setWidget(self.sets_widget)
        self.sets_widget.setAutoFillBackground(False)
        sets_group.layout().addWidget(scrollarea)
        self.layout().addWidget(sets_group, 0, 2, 3, 1)
        self.layout().setColumnStretch(2, 1)

    @QtSlot(bool)
    @catch_all
    def enable_ff(self, value):
        self.privacy['friends'].setEnabled(self.privacy['private'].isChecked())
        self.privacy['family'].setEnabled(self.privacy['private'].isChecked())

    def get_fixed_params(self):
        is_public = str(int(self.privacy['public'].isChecked()))
        is_family = str(int(self.privacy['private'].isChecked() and
                            self.privacy['family'].isChecked()))
        is_friend = str(int(self.privacy['private'].isChecked() and
                            self.privacy['friends'].isChecked()))
        if self.content_type['photo'].isChecked():
            content_type = '1'
        elif self.content_type['screenshot'].isChecked():
            content_type = '2'
        else:
            content_type = '3'
        hidden = str(int(self.hidden.isChecked()))
        return {
            'permissions': {
                'is_public': is_public,
                'is_friend': is_friend,
                'is_family': is_family,
                },
            'content_type': {'content_type': content_type},
            'hidden'      : {'hidden'      : hidden},
            }

    def clear_sets(self):
        for child in self.sets_widget.children():
            if child.isWidgetType():
                self.sets_widget.layout().removeWidget(child)
                child.setParent(None)

    def checked_sets(self):
        result = []
        for child in self.sets_widget.children():
            if child.isWidgetType() and child.isChecked():
                result.append(child)
        return result

    def add_set(self, title, description, photoset_id, index=-1):
        widget = QtWidgets.QCheckBox(title.replace('&', '&&'))
        if description:
            widget.setToolTip(html.unescape(description))
        widget.setProperty('photoset_id', photoset_id)
        if index >= 0:
            self.sets_widget.layout().insertWidget(index, widget)
        else:
            self.sets_widget.layout().addWidget(widget)
        return widget
Ejemplo n.º 22
0
class SpellCheck(QtCore.QObject):
    new_dict = QtSignal()

    def __init__(self, *arg, **kw):
        super(SpellCheck, self).__init__(*arg, **kw)
        self.config_store = QtWidgets.QApplication.instance().config_store
        self.enable(self.config_store.get('spelling', 'enabled', True))
        self.set_language(self.config_store.get('spelling', 'language'))

    @staticmethod
    def available_languages():
        result = defaultdict(list)
        if Gspell:
            for lang in Gspell.Language.get_available():
                code = lang.get_code()
                name = lang.get_name()
                match = re.match('(.+)\s+\((.+?)\)', name)
                if match:
                    language = match.group(1)
                    country = match.group(2)
                    if country == 'any':
                        country = ''
                else:
                    language = name
                    country = ''
                result[language].append((country, code))
        elif enchant:
            for code in enchant.list_languages():
                locale = QtCore.QLocale(code)
                language = locale.languageToString(locale.language())
                if '_' in code and '_ANY' not in code:
                    country = locale.countryToString(locale.country())
                else:
                    country = ''
                result[language].append((country, code))
        else:
            return None
        for value in result.values():
            value.sort()
        return dict(result) or None

    def current_language(self):
        if not self.dict:
            return ''
        if Gspell:
            language = self.dict.get_language()
            if language:
                return language.get_code()
        elif enchant:
            return self.dict.tag
        return ''

    @QtSlot(bool)
    @catch_all
    def enable(self, enabled):
        self.config_store.set('spelling', 'enabled', enabled)
        self.enabled = enabled and bool(Gspell or enchant)
        self.new_dict.emit()

    def set_language(self, code):
        if code:
            logger.debug('Setting dictionary %s', code)
        self.dict = None
        if Gspell:
            if code:
                self.dict = Gspell.Checker.new(Gspell.Language.lookup(code))
        elif enchant:
            if code and enchant.dict_exists(code):
                self.dict = enchant.Dict(code)
        else:
            return
        if code and not self.dict:
            logger.warning('Failed to set dictionary %s', code)
        self.config_store.set('spelling', 'language', self.current_language())
        self.new_dict.emit()

    words = re.compile(r"\w+([-'’]\w+)*", flags=re.IGNORECASE | re.UNICODE)

    def find_words(self, text):
        for word in self.words.finditer(text):
            yield word.group(), word.start(), word.end()

    def check(self, word):
        if not (word and self.enabled and self.dict):
            return True
        if word.isnumeric():
            return True
        if Gspell:
            return self.dict.check_word(word, -1)
        if enchant:
            return self.dict.check(word)
        return True

    def suggest(self, word):
        if self.check(word):
            return []
        if Gspell:
            return GSListPtr_to_list(self.dict.get_suggestions(word, -1))
        if enchant:
            return self.dict.suggest(word)
        return []
Ejemplo n.º 23
0
class SpellCheck(QtCore.QObject):
    new_dict = QtSignal()

    def __init__(self, *arg, **kw):
        super(SpellCheck, self).__init__(*arg, **kw)
        self.config_store = QtWidgets.QApplication.instance().config_store
        self.enable(eval(self.config_store.get('spelling', 'enabled', 'True')))
        self.set_language(self.config_store.get('spelling', 'language'))

    @staticmethod
    def available_languages():
        result = []
        if Gspell:
            for lang in Gspell.Language.get_available():
                result.append((lang.get_name(), lang.get_code()))
        elif enchant:
            languages = enchant.list_languages()
            languages.sort()
            for lang in languages:
                result.append((lang, lang))
        else:
            return None
        return result

    def current_language(self):
        if not self.dict:
            return ''
        if Gspell:
            language = self.dict.get_language()
            if language:
                return language.get_code()
        elif enchant:
            return self.dict.tag
        return ''

    @QtSlot(bool)
    @catch_all
    def enable(self, enabled):
        self.enabled = enabled and bool(Gspell or enchant)
        self.config_store.set('spelling', 'enabled', str(self.enabled))
        self.new_dict.emit()

    def set_language(self, code):
        if code:
            logger.debug('Setting dictionary %s', code)
        self.dict = None
        if Gspell:
            if code:
                self.dict = Gspell.Checker.new(Gspell.Language.lookup(code))
        elif enchant:
            if code and enchant.dict_exists(code):
                self.dict = enchant.Dict(code)
        if code and not self.dict:
            logger.warning('Failed to set dictionary %s', code)
        self.config_store.set('spelling', 'language', self.current_language())
        self.new_dict.emit()

    words = re.compile(r"\w+([-'’]\w+)*", flags=re.IGNORECASE | re.UNICODE)

    def find_words(self, text):
        for word in self.words.finditer(text):
            yield word.group(), word.start(), word.end()

    def check(self, word):
        if not (word and self.enabled and self.dict):
            return True
        if Gspell:
            return self.dict.check_word(word, -1)
        if enchant:
            return self.dict.check(word)
        return True

    def suggest(self, word):
        if self.check(word):
            return []
        if Gspell:
            return GSListPtr_to_list(self.dict.get_suggestions(word, -1))
        if enchant:
            return self.dict.suggest(word)
        return []
Ejemplo n.º 24
0
class LocationInfo(QtWidgets.QWidget):
    new_value = QtSignal(object, dict)

    def __init__(self, *args, **kw):
        super(LocationInfo, self).__init__(*args, **kw)
        layout = QtWidgets.QGridLayout()
        self.setLayout(layout)
        layout.setContentsMargins(0, 0, 0, 0)
        self.members = {}
        for key in ('SubLocation', 'City', 'ProvinceState', 'CountryName',
                    'CountryCode', 'WorldRegion'):
            self.members[key] = SingleLineEdit(
                length_check=ImageMetadata.max_bytes(key))
            self.members[key].editingFinished.connect(self.editing_finished)
        self.members['CountryCode'].setMaximumWidth(
            width_for_text(self.members['CountryCode'], 'W' * 4))
        self.members['SubLocation'].setToolTip(
            translate('AddressTab', 'Enter the name of the sublocation.'))
        self.members['City'].setToolTip(
            translate('AddressTab', 'Enter the name of the city.'))
        self.members['ProvinceState'].setToolTip(
            translate('AddressTab',
                      'Enter the name of the province or state.'))
        self.members['CountryName'].setToolTip(
            translate('AddressTab', 'Enter the name of the country.'))
        self.members['CountryCode'].setToolTip(
            translate(
                'AddressTab',
                'Enter the 2 or 3 letter ISO 3166 country code of the country.'
            ))
        self.members['WorldRegion'].setToolTip(
            translate('AddressTab', 'Enter the name of the world region.'))
        for j, text in enumerate((
                translate('AddressTab', 'Street'),
                translate('AddressTab', 'City'),
                translate('AddressTab', 'Province'),
                translate('AddressTab', 'Country'),
                translate('AddressTab', 'Region'),
        )):
            label = QtWidgets.QLabel(text)
            label.setAlignment(Qt.AlignRight)
            layout.addWidget(label, j, 0)
        layout.addWidget(self.members['SubLocation'], 0, 1, 1, 2)
        layout.addWidget(self.members['City'], 1, 1, 1, 2)
        layout.addWidget(self.members['ProvinceState'], 2, 1, 1, 2)
        layout.addWidget(self.members['CountryName'], 3, 1)
        layout.addWidget(self.members['CountryCode'], 3, 2)
        layout.addWidget(self.members['WorldRegion'], 4, 1, 1, 2)
        layout.setRowStretch(5, 1)

    def get_value(self):
        new_value = {}
        for key in self.members:
            if self.members[key].is_multiple():
                continue
            new_value[key] = self.members[key].get_value().strip() or None
        return new_value

    @QtSlot()
    @catch_all
    def editing_finished(self):
        self.new_value.emit(self, self.get_value())
Ejemplo n.º 25
0
class ScrollArea(QtWidgets.QScrollArea):
    dropped_images = QtSignal(list)
    multi_row_changed = QtSignal()

    def __init__(self, parent=None):
        super(ScrollArea, self).__init__(parent)
        self.multi_row = None
        self.set_multi_row(True)
        self.setWidgetResizable(True)
        self.setAcceptDrops(True)
        widget = QtWidgets.QWidget()
        self.thumbs = ThumbsLayout(scroll_area=self)
        widget.setLayout(self.thumbs)
        self.setWidget(widget)
        # adopt some layout methods
        self.add_widget = self.thumbs.addWidget
        self.remove_widget = self.thumbs.removeWidget

    def set_multi_row(self, multi_row):
        if multi_row:
            self.setMinimumHeight(0)
        else:
            scrollbar = self.horizontalScrollBar()
            self.setMinimumHeight(self.thumbs.sizeHint().height() +
                                  scrollbar.height())
        if multi_row == self.multi_row:
            return
        self.multi_row = multi_row
        if multi_row:
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        else:
            self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
            self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.multi_row_changed.emit()

    def ensureWidgetVisible(self, widget):
        left, top, right, bottom = self.thumbs.getContentsMargins()
        super(ScrollArea, self).ensureWidgetVisible(widget, max(left, right),
                                                    max(top, bottom))

    @catch_all
    def dropEvent(self, event):
        file_list = []
        for uri in event.mimeData().urls():
            file_list.append(uri.toLocalFile())
        if file_list:
            self.dropped_images.emit(file_list)

    @catch_all
    def dragEnterEvent(self, event):
        if event.mimeData().hasFormat('text/uri-list'):
            event.acceptProposedAction()

    @catch_all
    def resizeEvent(self, event):
        super(ScrollArea, self).resizeEvent(event)
        width = event.size().width()
        height = event.size().height()
        if not self.multi_row:
            scrollbar = self.verticalScrollBar()
            width -= scrollbar.width()
        scrollbar = self.horizontalScrollBar()
        if not scrollbar.isVisible():
            height -= scrollbar.height()
        self.thumbs.set_viewport_size(QtCore.QSize(width, height))
Ejemplo n.º 26
0
class ImageList(QtWidgets.QWidget):
    image_list_changed = QtSignal()
    new_metadata = QtSignal(bool)
    selection_changed = QtSignal(list)
    sort_order_changed = QtSignal()

    def __init__(self, parent=None):
        super(ImageList, self).__init__(parent)
        self.app = QtWidgets.QApplication.instance()
        self.drag_icon = None
        self.images = []
        self.last_selected = None
        self.selection_anchor = None
        self.thumb_size = int(
            self.app.config_store.get('controls', 'thumb_size', '80'))
        layout = QtWidgets.QGridLayout()
        layout.setSpacing(0)
        layout.setRowStretch(0, 1)
        layout.setColumnStretch(3, 1)
        self.setLayout(layout)
        layout.setContentsMargins(0, 0, 0, 0)
        # thumbnail display
        self.scroll_area = ScrollArea()
        self.scroll_area.dropped_images.connect(self.open_file_list)
        self.scroll_area.multi_row_changed.connect(
            self._ensure_selected_visible)
        layout.addWidget(self.scroll_area, 0, 0, 1, 6)
        QtWidgets.QShortcut(QtGui.QKeySequence.MoveToPreviousChar,
                            self.scroll_area, self.move_to_prev_thumb)
        QtWidgets.QShortcut(QtGui.QKeySequence.MoveToNextChar,
                            self.scroll_area, self.move_to_next_thumb)
        QtWidgets.QShortcut(QtGui.QKeySequence.MoveToStartOfLine,
                            self.scroll_area, self.move_to_first_thumb)
        QtWidgets.QShortcut(QtGui.QKeySequence.MoveToEndOfLine,
                            self.scroll_area, self.move_to_last_thumb)
        QtWidgets.QShortcut(QtGui.QKeySequence.SelectPreviousChar,
                            self.scroll_area, self.select_prev_thumb)
        QtWidgets.QShortcut(QtGui.QKeySequence.SelectNextChar,
                            self.scroll_area, self.select_next_thumb)
        QtWidgets.QShortcut(QtGui.QKeySequence.SelectAll, self.scroll_area,
                            self.select_all)
        # sort key selector
        layout.addWidget(QtWidgets.QLabel(self.tr('sort by: ')), 1, 0)
        self.sort_name = QtWidgets.QRadioButton(self.tr('file name'))
        self.sort_name.clicked.connect(self._new_sort_order)
        layout.addWidget(self.sort_name, 1, 1)
        self.sort_date = QtWidgets.QRadioButton(self.tr('date taken'))
        layout.addWidget(self.sort_date, 1, 2)
        self.sort_date.clicked.connect(self._new_sort_order)
        if eval(self.app.config_store.get('controls', 'sort_date', 'False')):
            self.sort_date.setChecked(True)
        else:
            self.sort_name.setChecked(True)
        # size selector
        layout.addWidget(QtWidgets.QLabel(self.tr('thumbnail size: ')), 1, 4)
        self.size_slider = QtWidgets.QSlider(Qt.Horizontal)
        self.size_slider.setTracking(False)
        self.size_slider.setRange(4, 9)
        self.size_slider.setPageStep(1)
        self.size_slider.setValue(self.thumb_size // 20)
        self.size_slider.setTickPosition(QtWidgets.QSlider.TicksBelow)
        width = self.size_slider.sizeHint().width()
        self.size_slider.setMinimumWidth(width * 7 // 4)
        self.size_slider.valueChanged.connect(self._new_thumb_size)
        layout.addWidget(self.size_slider, 1, 5)

    def set_drag_to_map(self, icon, hotspot=None):
        self.drag_icon = icon
        self.drag_hotspot = hotspot

    def get_image(self, path):
        for image in self.images:
            if image.path == path:
                return image
        return None

    def get_images(self):
        return self.images

    @catch_all
    def mousePressEvent(self, event):
        if self.scroll_area.underMouse():
            self._clear_selection()
            self.last_selected = None
            self.selection_anchor = None
            self.emit_selection()

    @QtSlot(bool)
    @catch_all
    def open_files(self, checked=False):
        args = [
            self,
            self.tr('Open files'),
            self.app.config_store.get('paths', 'images', ''),
            self.tr("Images ({0});;Videos ({1});;All files (*)").format(
                ' '.join(['*.' + x for x in image_types()]),
                ' '.join(['*.' + x for x in video_types()]))
        ]
        if eval(self.app.config_store.get('pyqt', 'native_dialog', 'True')):
            pass
        else:
            args += [None, QtWidgets.QFileDialog.DontUseNativeDialog]
        path_list = QtWidgets.QFileDialog.getOpenFileNames(*args)
        path_list = path_list[0]
        if not path_list:
            return
        self.open_file_list(path_list)

    @QtSlot(list)
    @catch_all
    def open_file_list(self, path_list):
        with Busy():
            for path in path_list:
                self.open_file(path)
        self.done_opening(path_list[-1])

    def open_file(self, path):
        path = os.path.abspath(path)
        if not os.path.isfile(path):
            return
        if self.get_image(path):
            # already opened this path
            return
        image = Image(path, self, thumb_size=self.thumb_size)
        self.images.append(image)
        self.show_thumbnail(image)

    def done_opening(self, path):
        self.app.config_store.set('paths', 'images',
                                  os.path.dirname(os.path.abspath(path)))
        self._sort_thumbnails()

    def _date_key(self, image):
        result = image.metadata.date_taken
        if result is None:
            result = image.metadata.date_digitised
        if result is None:
            result = image.metadata.date_modified
        if result is None:
            # use file date as last resort
            result = datetime.fromtimestamp(os.path.getmtime(image.path))
        else:
            result = result['datetime']
        # convert result to string and append path so photos with same
        # time stamp get sorted consistently
        result = result.strftime('%Y%m%d%H%M%S%f') + image.path
        return result

    @QtSlot()
    @catch_all
    def _new_sort_order(self):
        self._sort_thumbnails()
        self.sort_order_changed.emit()

    @QtSlot()
    @catch_all
    def _ensure_selected_visible(self):
        if self.last_selected:
            self.scroll_area.ensureWidgetVisible(self.last_selected)

    def _sort_thumbnails(self):
        sort_date = self.sort_date.isChecked()
        self.app.config_store.set('controls', 'sort_date', str(sort_date))
        with Busy():
            if sort_date:
                self.images.sort(key=self._date_key)
            else:
                self.images.sort(key=lambda x: x.path)
            for image in self.images:
                self.show_thumbnail(image, False)
        if self.last_selected:
            self.app.processEvents()
            self.scroll_area.ensureWidgetVisible(self.last_selected)
        self.image_list_changed.emit()

    def show_thumbnail(self, image, live=True):
        self.scroll_area.add_widget(image)
        if live:
            self.app.processEvents()
        image.load_thumbnail()
        if live:
            self.app.processEvents()
            self.scroll_area.ensureWidgetVisible(image)
            self.app.processEvents()

    def add_selected_actions(self, menu):
        actions = {}
        actions['reload'] = menu.addAction('', self.reload_selected_metadata)
        actions['save'] = menu.addAction(
            translate('ImageList', 'Save changes'),
            self.save_selected_metadata)
        actions['diff'] = menu.addAction(
            translate('ImageList', 'View changes'),
            self.diff_selected_metadata)
        actions['thumbs'] = menu.addAction('',
                                           self.regenerate_selected_thumbnails)
        actions['close'] = menu.addAction('', self.close_selected_files)
        self.configure_selected_actions(actions)
        return actions

    def configure_selected_actions(self, actions):
        images = self.get_selected_images()
        changed_images = any([x.metadata.changed() for x in images])
        actions['reload'].setEnabled(bool(images))
        actions['save'].setEnabled(changed_images)
        actions['diff'].setEnabled(changed_images)
        actions['thumbs'].setEnabled(bool(images))
        actions['close'].setEnabled(bool(images))
        actions['reload'].setText(
            translate('ImageList', 'Reload file(s)', '', len(images)))
        actions['thumbs'].setText(
            translate('ImageList', 'Regenerate thumbnail(s)', '', len(images)))
        actions['close'].setText(
            translate('ImageList', 'Close file(s)', '', len(images)))

    @QtSlot()
    @catch_all
    def reload_selected_metadata(self):
        with Busy():
            for image in self.get_selected_images():
                image.reload_metadata()

    @QtSlot()
    @catch_all
    def save_selected_metadata(self):
        self._save_files(images=self.get_selected_images())

    @QtSlot()
    @catch_all
    def diff_selected_metadata(self):
        dialog = QtWidgets.QDialog(parent=self)
        dialog.setLayout(QtWidgets.QVBoxLayout())
        dialog.setFixedSize(min(800,
                                self.window().width()),
                            min(400,
                                self.window().height()))
        table = QtWidgets.QTableWidget()
        table.setColumnCount(3)
        table.setHorizontalHeaderLabels([
            translate('ImageList', 'new value'),
            translate('ImageList', 'undo'),
            translate('ImageList', 'old value')
        ])
        table.horizontalHeader().setSectionResizeMode(
            0, QtWidgets.QHeaderView.Stretch)
        table.horizontalHeader().setSectionResizeMode(
            2, QtWidgets.QHeaderView.Stretch)
        dialog.layout().addWidget(table)
        button_box = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
        button_box.accepted.connect(dialog.accept)
        button_box.rejected.connect(dialog.reject)
        dialog.layout().addWidget(button_box)
        changed = False
        position = None
        for image in self.get_selected_images():
            if not image.metadata.changed():
                continue
            dialog.setWindowTitle(
                translate('ImageList',
                          'Metadata differences: {}').format(image.name))
            labels = []
            row = 0
            undo = {}
            table.clearContents()
            new_md = image.metadata
            old_md = Metadata(image.path)
            for key in ('title', 'description', 'keywords', 'rating',
                        'copyright', 'creator', 'date_taken', 'date_digitised',
                        'date_modified', 'orientation', 'camera_model',
                        'lens_model', 'lens_spec', 'focal_length',
                        'focal_length_35', 'aperture', 'latlong', 'altitude',
                        'location_taken', 'location_shown', 'thumbnail'):
                values = getattr(new_md, key), getattr(old_md, key)
                if values[0] == values[1]:
                    continue
                values = [str(x or '') for x in values]
                table.setRowCount(row + 1)
                for n, value in enumerate(values):
                    item = QtWidgets.QTableWidgetItem(value)
                    table.setItem(row, n * 2, item)
                undo[key] = QtWidgets.QTableWidgetItem()
                undo[key].setFlags(undo[key].flags() | Qt.ItemIsUserCheckable)
                undo[key].setCheckState(Qt.Unchecked)
                table.setItem(row, 1, undo[key])
                labels.append(key)
                row += 1
            if not row:
                continue
            table.setVerticalHeaderLabels(labels)
            table.resizeColumnsToContents()
            table.resizeRowsToContents()
            if position:
                dialog.move(position)
            if dialog.exec_() != QtWidgets.QDialog.Accepted:
                return
            position = dialog.pos()
            undo_all = True
            for key, widget in undo.items():
                if widget.checkState() == Qt.Checked:
                    setattr(new_md, key, getattr(old_md, key))
                    changed = True
                else:
                    undo_all = False
            if undo_all:
                image.reload_metadata()
        if changed:
            self.emit_selection()

    @QtSlot()
    @catch_all
    def regenerate_selected_thumbnails(self):
        with Busy():
            for image in self.get_selected_images():
                if image.regenerate_thumbnail():
                    image.load_thumbnail()

    @QtSlot()
    @catch_all
    def close_selected_files(self):
        self.close_files(False)

    @QtSlot()
    @catch_all
    def close_all_files(self):
        self.close_files(True)

    def close_files(self, all_files):
        if not self.unsaved_files_dialog(all_files=all_files):
            return
        if all_files:
            close_list = list(self.images)
        else:
            close_list = self.get_selected_images()
        if not close_list:
            return
        idx = self.images.index(close_list[0])
        for image in close_list:
            self.images.remove(image)
            self.scroll_area.remove_widget(image)
            image.setParent(None)
        if 0 <= idx < len(self.images):
            self.select_image(self.images[idx])
        else:
            self.last_selected = None
            self.selection_anchor = None
            self.emit_selection()
        self.image_list_changed.emit()

    @QtSlot(bool)
    @catch_all
    def save_files(self, checked=False):
        self._save_files(self.images)

    def _save_files(self, images=[]):
        if_mode = eval(self.app.config_store.get('files', 'image', 'True'))
        sc_mode = self.app.config_store.get('files', 'sidecar', 'auto')
        force_iptc = eval(
            self.app.config_store.get('files', 'force_iptc', 'False'))
        keep_time = eval(
            self.app.config_store.get('files', 'preserve_timestamps', 'False'))
        if not images:
            images = self.images
        with Busy():
            for image in images:
                if keep_time:
                    file_times = image.file_times
                else:
                    file_times = None
                image.metadata.save(if_mode=if_mode,
                                    sc_mode=sc_mode,
                                    force_iptc=force_iptc,
                                    file_times=file_times)
        unsaved = False
        for image in self.images:
            if image.metadata.changed():
                unsaved = True
                break
        self.new_metadata.emit(unsaved)

    def unsaved_files_dialog(self,
                             all_files=False,
                             with_cancel=True,
                             with_discard=True):
        """Return true if OK to continue with close or quit or whatever"""
        for image in self.images:
            if image.metadata.changed() and (all_files or image.selected):
                break
        else:
            return True
        dialog = QtWidgets.QMessageBox(parent=self)
        dialog.setWindowTitle(self.tr('Photini: unsaved data'))
        dialog.setText(self.tr('<h3>Some images have unsaved metadata.</h3>'))
        dialog.setInformativeText(self.tr('Do you want to save your changes?'))
        dialog.setIcon(QtWidgets.QMessageBox.Warning)
        buttons = QtWidgets.QMessageBox.Save
        if with_cancel:
            buttons |= QtWidgets.QMessageBox.Cancel
        if with_discard:
            buttons |= QtWidgets.QMessageBox.Discard
        dialog.setStandardButtons(buttons)
        dialog.setDefaultButton(QtWidgets.QMessageBox.Save)
        result = dialog.exec_()
        if result == QtWidgets.QMessageBox.Save:
            self._save_files()
            return True
        return result == QtWidgets.QMessageBox.Discard

    def get_selected_images(self):
        selection = []
        for image in self.images:
            if image.get_selected():
                selection.append(image)
        return selection

    def emit_selection(self):
        self.selection_changed.emit(self.get_selected_images())

    def select_all(self):
        for image in self.images:
            image.set_selected(True)
        self.selection_anchor = None
        self.last_selected = None
        self.emit_selection()

    def move_to_prev_thumb(self):
        self._inc_selection(-1)

    def move_to_next_thumb(self):
        self._inc_selection(1)

    def move_to_first_thumb(self):
        self.select_image(self.images[0])

    def move_to_last_thumb(self):
        self.select_image(self.images[-1])

    def select_prev_thumb(self):
        self._inc_selection(-1, extend_selection=True)

    def select_next_thumb(self):
        self._inc_selection(1, extend_selection=True)

    def _inc_selection(self, inc, extend_selection=False):
        if self.last_selected:
            idx = self.images.index(self.last_selected)
            idx = (idx + inc) % len(self.images)
        else:
            idx = 0
        self.select_image(self.images[idx], extend_selection=extend_selection)

    @QtSlot(int)
    @catch_all
    def _new_thumb_size(self, value):
        self.thumb_size = value * 20
        self.app.config_store.set('controls', 'thumb_size',
                                  str(self.thumb_size))
        for image in self.images:
            image.set_thumb_size(self.thumb_size)
        if self.last_selected:
            self.app.processEvents()
            self.scroll_area.ensureWidgetVisible(self.last_selected)

    def select_image(self,
                     image,
                     extend_selection=False,
                     multiple_selection=False):
        self.scroll_area.ensureWidgetVisible(image)
        if extend_selection and self.selection_anchor:
            idx1 = self.images.index(self.selection_anchor)
            idx2 = self.images.index(self.last_selected)
            for i in range(min(idx1, idx2), max(idx1, idx2) + 1):
                self.images[i].set_selected(False)
            idx2 = self.images.index(image)
            for i in range(min(idx1, idx2), max(idx1, idx2) + 1):
                self.images[i].set_selected(True)
        elif multiple_selection:
            image.set_selected(not image.get_selected())
            self.selection_anchor = image
        else:
            self._clear_selection()
            image.set_selected(True)
            self.selection_anchor = image
        self.last_selected = image
        self.emit_selection()

    def select_images(self, images):
        self._clear_selection()
        if not images:
            self.last_selected = None
            self.selection_anchor = None
            self.emit_selection()
            return
        for image in images:
            image.set_selected(True)
            self.scroll_area.ensureWidgetVisible(image)
        self.selection_anchor = images[0]
        self.last_selected = images[-1]
        self.emit_selection()

    def _clear_selection(self):
        for image in self.images:
            if image.get_selected():
                image.set_selected(False)