Exemplo 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 = QtCore.Signal()
    write_text = QtCore.Signal(str)

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

    def flush(self):
        self.flush_text.emit()
Exemplo n.º 2
0
class FileCopier(QtCore.QObject):
    output = QtCore.Signal(dict, six.text_type)

    def __init__(self, source, copy_list, move, updating, *args, **kwds):
        super(FileCopier, self).__init__(*args, **kwds)
        self.source = source
        self.copy_list = copy_list
        self.move = move
        self.updating = updating
        self.running = True

    @QtCore.Slot()
    @catch_all
    def start(self):
        status = 'ok'
        try:
            for info in self.source.copy_files(self.copy_list, self.move):
                # don't display image until previous one has been displayed
                self.updating.lock()
                self.output.emit(info, status)
                self.updating.unlock()
                if not self.running:
                    break
        except Exception as ex:
            status = str(ex)
            logger.error(status)
        self.output.emit({}, status)
Exemplo n.º 3
0
class AuthServer(QtCore.QObject):
    finished = QtCore.Signal()
    response = QtCore.Signal(dict)

    @QtCore.Slot()
    @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()
Exemplo n.º 4
0
class DateLink(QtWidgets.QCheckBox):
    new_link = QtCore.Signal(six.text_type)

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

    @QtCore.Slot()
    @catch_all
    def _clicked(self):
        self.new_link.emit(self.name)
Exemplo n.º 5
0
class GoogleUploadConfig(QtWidgets.QWidget):
    new_set = QtCore.Signal()

    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
Exemplo n.º 6
0
class DateAndTimeWidget(QtWidgets.QGridLayout):
    new_value = QtCore.Signal(six.text_type, 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]:
                new_value[key] = new_value[key].toPyDateTime()
        return new_value

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

    @QtCore.Slot()
    @catch_all
    def log_out(self):
        keyring.delete_password('photini', self.name)
        self.disconnect()

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

    def set_password(self, password):
        keyring.set_password('photini', self.name, password)
Exemplo n.º 8
0
class DropdownEdit(ComboBox):
    new_value = QtCore.Signal(object)

    def __init__(self, *arg, **kw):
        super(DropdownEdit, self).__init__(*arg, **kw)
        self.addItem(translate('TechnicalTab', '<clear>'), None)
        self.addItem('', None)
        self.setItemData(1, 0, Qt.UserRole - 1)
        self.addItem(multiple_values(), None)
        self.setItemData(2, 0, Qt.UserRole - 1)
        self.currentIndexChanged.connect(self.current_index_changed)

    @QtCore.Slot(int)
    @catch_all
    def current_index_changed(self, int):
        self.new_value.emit(self.get_value())

    def add_item(self, text, data):
        blocked = self.blockSignals(True)
        self.insertItem(self.count() - 3, text, six.text_type(data))
        self.set_dropdown_width()
        self.blockSignals(blocked)

    def remove_item(self, data):
        blocked = self.blockSignals(True)
        self.removeItem(self.findData(six.text_type(data)))
        self.set_dropdown_width()
        self.blockSignals(blocked)

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

    def set_value(self, value):
        blocked = self.blockSignals(True)
        if not value:
            self.setCurrentIndex(self.count() - 2)
        else:
            self.setCurrentIndex(self.findData(six.text_type(value)))
        self.blockSignals(blocked)

    def get_value(self):
        return self.itemData(self.currentIndex())

    def set_multiple(self):
        blocked = self.blockSignals(True)
        self.setCurrentIndex(self.count() - 1)
        self.blockSignals(blocked)
Exemplo n.º 9
0
class LatLongDisplay(SingleLineEdit):
    changed = QtCore.Signal()

    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(170)
        self.setEnabled(False)
        self.editingFinished.connect(self.editing_finished)

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

    def refresh(self):
        images = self.image_list.get_selected_images()
        if not images:
            self.set_value(None)
            self.setEnabled(False)
            return
        values = []
        for image in 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)
Exemplo n.º 10
0
class LocationInfo(QtWidgets.QWidget):
    new_value = QtCore.Signal(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

    @QtCore.Slot()
    @catch_all
    def editing_finished(self):
        self.new_value.emit(self, self.get_value())
Exemplo n.º 11
0
class NameMangler(QtCore.QObject):
    number_parser = re.compile(r'(\d+)')
    new_example = QtCore.Signal(str)

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

    @QtCore.Slot(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)
Exemplo n.º 12
0
class PrecisionSlider(Slider):
    value_changed = QtCore.Signal(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)
Exemplo n.º 13
0
class MapWebView(QWebView):
    drop_text = QtCore.Signal(int, int, six.text_type)

    def __init__(self, call_handler, *args, **kwds):
        super(MapWebView, self).__init__(*args, **kwds)
        # set view's page
        if using_qtwebengine:
            self.setPage(MapWebEnginePage(parent=self))
            self.page().set_call_handler(call_handler)
            self.settings().setAttribute(
                QWebSettings.Accelerated2dCanvasEnabled, False)
        else:
            self.setPage(MapWebKitPage(call_handler, parent=self))
        self.settings().setAttribute(
            QWebSettings.LocalContentCanAccessRemoteUrls, True)
        self.settings().setAttribute(
            QWebSettings.LocalContentCanAccessFileUrls, True)

    @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)
Exemplo n.º 14
0
class ImageList(QtWidgets.QWidget):
    image_list_changed = QtCore.Signal()
    new_metadata = QtCore.Signal(bool)
    selection_changed = QtCore.Signal(list)
    sort_order_changed = QtCore.Signal()

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

    @QtCore.Slot(bool)
    @catch_all
    def open_files(self, checked):
        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
        elif qt_version_info >= (5, 0):
            args += [None, QtWidgets.QFileDialog.DontUseNativeDialog]
        else:
            args += [QtWidgets.QFileDialog.DontUseNativeDialog]
        path_list = QtWidgets.QFileDialog.getOpenFileNames(*args)
        if qt_version_info >= (5, 0):
            path_list = path_list[0]
        if not path_list:
            return
        # work around for Qt bug 33992
        # https://bugreports.qt-project.org/browse/QTBUG-33992
        if qt_version_info in ((4, 8, 4), (4, 8, 5)):
            path_list = list(map(unquote, path_list))
        self.open_file_list(path_list)

    @QtCore.Slot(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(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

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

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

    @QtCore.Slot(bool)
    @catch_all
    def save_files(self, checked):
        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()
        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)

    @QtCore.Slot(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)
Exemplo n.º 15
0
class ScrollArea(QtWidgets.QScrollArea):
    dropped_images = QtCore.Signal(list)

    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)

    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))
Exemplo n.º 16
0
class UploadWorker(QtCore.QObject):
    finished = QtCore.Signal()
    upload_error = QtCore.Signal(six.text_type, six.text_type)
    upload_progress = QtCore.Signal(float, six.text_type)

    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

    @QtCore.Slot()
    @catch_all
    def start(self):
        session = self.session_factory()
        session.connect()
        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.disconnect()
        self.finished.emit()

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

    @QtCore.Slot(bool)
    @catch_all
    def abort_upload(self, retry):
        self.retry = retry
        if self.fileobj:
            # brutal way to interrupt an upload
            self.fileobj.abort()
Exemplo n.º 17
0
class PhotiniUploader(QtWidgets.QWidget):
    abort_upload = QtCore.Signal(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)

    @QtCore.Slot()
    @catch_all
    def shutdown(self):
        self.session.disconnect()

    @QtCore.Slot(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._sc or not image.metadata._if.has_iptc():
                # 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

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

    @QtCore.Slot()
    @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()

    @QtCore.Slot(float, six.text_type)
    @catch_all
    def upload_progress(self, value, format_):
        self.total_progress.setValue(value)
        if format_:
            self.total_progress.setFormat(format_)

    @QtCore.Slot(six.text_type, six.text_type)
    @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)

    @QtCore.Slot()
    @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()

    @QtCore.Slot()
    @catch_all
    def log_in(self, do_auth=True):
        with DisableWidget(self.user_connect):
            with Busy():
                connect = self.session.connect()
            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')

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

    @QtCore.Slot(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)
Exemplo n.º 18
0
class Metadata(QtCore.QObject):
    unsaved = QtCore.Signal(bool)

    # type of each Photini data field's data
    _data_type = {
        'altitude': Altitude,
        'aperture': Aperture,
        'camera_model': CameraModel,
        'copyright': MD_String,
        'creator': MultiString,
        'date_digitised': DateTime,
        'date_modified': DateTime,
        'date_taken': DateTime,
        'description': MD_String,
        'dimension_x': MD_Int,
        'dimension_y': MD_Int,
        'focal_length': MD_Rational,
        'focal_length_35': MD_Int,
        'keywords': MultiString,
        'latlong': LatLon,
        'lens_make': MD_String,
        'lens_model': MD_String,
        'lens_serial': MD_String,
        'lens_spec': LensSpec,
        'location_shown': MultiLocation,
        'location_taken': Location,
        'orientation': Orientation,
        'rating': Rating,
        'resolution_x': MD_Rational,
        'resolution_y': MD_Rational,
        'resolution_unit': MD_Int,
        'software': Software,
        'thumbnail': Thumbnail,
        'timezone': Timezone,
        'title': MD_String,
    }

    def __init__(self, path, *args, **kw):
        super(Metadata, self).__init__(*args, **kw)
        # create metadata handlers for image file, video file, and sidecar
        self._path = path
        self._vf = None
        self._sc = SidecarMetadata.open_old(path)
        self._if = ImageMetadata.open_old(path)
        self.mime_type = self.get_mime_type()
        if self.mime_type.split('/')[0] == 'video':
            vhm = VideoHeaderMetadata.open_old(path)
            if vhm and self._if:
                vhm.merge_segment(self._if)
            self._if = vhm
            self._vf = FFMPEGMetadata.open_old(path)
        self.dirty = False

    @classmethod
    def clone(cls, path, other, *args, **kw):
        if other._if:
            # use exiv2 to clone image file metadata
            other._if.save_file(path)
        self = cls(path, *args, **kw)
        if other._sc and self._if:
            # merge in sidecar data
            self._if.merge_sc(other._sc)
        return self

    def _handler_save(self, handler, *arg, **kw):
        # store Photini metadata items
        for name in self._data_type:
            value = getattr(self, name)
            handler.write(name, value)
        # save file
        return handler.save(*arg, **kw)

    def save(self,
             if_mode=True,
             sc_mode='auto',
             force_iptc=False,
             file_times=None):
        if not self.dirty:
            return
        self.software = 'Photini editor v' + __version__
        OK = False
        try:
            # save to image file
            if self._if and if_mode:
                OK = self._handler_save(self._if,
                                        file_times=file_times,
                                        force_iptc=force_iptc)
            if not OK:
                # can't write to image file so must create side car
                sc_mode = 'always'
            # create side car
            if sc_mode == 'always' and not self._sc:
                self._sc = SidecarMetadata.open_new(self._path, self._if)
            # save or delete side car
            if self._sc:
                if sc_mode == 'delete':
                    self._sc = self._sc.delete()
                else:
                    # workaround for bug in exiv2 xmp timestamp altering
                    self._sc.clear_dates()
                    OK = self._handler_save(self._sc, file_times=file_times)
        except Exception as ex:
            logger.exception(ex)
            return
        if OK:
            self.dirty = False
            self.unsaved.emit(self.dirty)

    def get_mime_type(self):
        result = None
        if self._if:
            result = self._if.get_mime_type()
        if not result:
            result = mimetypes.guess_type(self._path)[0]
        if not result:
            result = imghdr.what(self._path)
            if result:
                result = 'image/' + result
        # anything not recognised is assumed to be 'raw'
        if not result:
            result = 'image/raw'
        return result

    def __getattr__(self, name):
        if name not in self._data_type:
            raise AttributeError("%s has no attribute %s" %
                                 (self.__class__, name))
        # read data values
        values = []
        for handler in self._sc, self._vf, self._if:
            if not handler:
                continue
            values = handler.read(name, self._data_type[name])
            if values:
                break
        # choose result and merge in non-matching data so user can review it
        result = None
        if values:
            info = '{}({})'.format(os.path.basename(self._path), name)
            tag, result = values.pop(0)
            logger.debug('%s: set from %s', info, tag)
        for tag, value in values:
            result = result.merge(info, tag, value)
        # merge in camera timezone if needed
        if (isinstance(result, DateTime) and result.tz_offset is None
                and self.timezone):
            result = dict(result)
            result['tz_offset'] = self.timezone
            result = DateTime(result)
            logger.info('%s: merged camera timezone offset', info)
        # add value to object attributes so __getattr__ doesn't get
        # called again
        super(Metadata, self).__setattr__(name, result)
        return result

    def __setattr__(self, name, value):
        if name not in self._data_type:
            return super(Metadata, self).__setattr__(name, value)
        if value in (None, '', [], {}):
            value = None
        elif not isinstance(value, self._data_type[name]):
            value = self._data_type[name](value)
            if not value:
                value = None
        if getattr(self, name) == value:
            return
        super(Metadata, self).__setattr__(name, value)
        if not self.dirty:
            self.dirty = True
        self.unsaved.emit(self.dirty)

    def changed(self):
        return self.dirty
Exemplo n.º 19
0
class AugmentSpinBox(object):
    new_value = QtCore.Signal(object)

    def __init__(self, *arg, **kw):
        super(AugmentSpinBox, self).__init__(*arg, **kw)
        self.set_value(None)
        self.editingFinished.connect(self.editing_finished)

    class ContextAction(QtWidgets.QAction):
        def __init__(self, label, value, parent):
            super(AugmentSpinBox.ContextAction, self).__init__(label, parent)
            self.setData(value)
            self.triggered.connect(self.set_value)

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

    @catch_all
    def contextMenuEvent(self, event):
        if self.specialValueText() and self.choices:
            QtCore.QTimer.singleShot(0, self.extend_context_menu)
        return super(self.__class__, self).contextMenuEvent(event)

    @QtCore.Slot()
    @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(self.textFromValue(suggestion), suggestion,
                                   self))

    @catch_all
    def keyPressEvent(self, event):
        if self.specialValueText():
            self.set_value(self.default_value)
            self.selectAll()
        return super(self.__class__, self).keyPressEvent(event)

    @catch_all
    def stepBy(self, steps):
        if self.specialValueText():
            self.set_value(self.default_value)
            self.selectAll()
        return super(self.__class__, self).stepBy(steps)

    @catch_all
    def fixup(self, text):
        if not self.cleanText():
            # user has deleted the value
            self.set_value(None)
            return ''
        return super(self.__class__, self).fixup(text)

    @QtCore.Slot()
    @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)
Exemplo n.º 20
0
class QTabBar(QtWidgets.QTabBar):
    context_menu = QtCore.Signal(QtGui.QContextMenuEvent)

    @catch_all
    def contextMenuEvent(self, event):
        self.context_menu.emit(event)
Exemplo n.º 21
0
class OffsetWidget(QtWidgets.QWidget):
    apply_offset = QtCore.Signal(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 = SquareButton(six.unichr(0x002b))
        add_button.setStyleSheet('QPushButton {padding: 0px}')
        set_symbol_font(add_button)
        scale_font(add_button, 170)
        add_button.clicked.connect(self.add)
        self.layout().addWidget(add_button)
        # subtract offset button
        sub_button = SquareButton(six.unichr(0x2212))
        sub_button.setStyleSheet('QPushButton {padding: 0px}')
        set_symbol_font(sub_button)
        scale_font(sub_button, 170)
        sub_button.clicked.connect(self.sub)
        self.layout().addWidget(sub_button)
        self.layout().addStretch(1)
        # restore stored values
        value = eval(self.config_store.get('technical', 'offset', 'None'))
        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()

    @QtCore.Slot()
    @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', str(value))

    @QtCore.Slot()
    @catch_all
    def add(self):
        self.do_inc(False)

    @QtCore.Slot()
    @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)
Exemplo n.º 22
0
class SpellCheck(QtCore.QObject):
    new_dict = QtCore.Signal()

    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 ''

    @QtCore.Slot(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 []
Exemplo n.º 23
0
class FlickrUploadConfig(QtWidgets.QWidget):
    new_set = QtCore.Signal()
    sync_metadata = QtCore.Signal()

    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)

    @QtCore.Slot(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