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()
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)
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()
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)
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
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())
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)
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)
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)
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())
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)
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)
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)
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)
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))
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()
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)
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
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)
class QTabBar(QtWidgets.QTabBar): context_menu = QtCore.Signal(QtGui.QContextMenuEvent) @catch_all def contextMenuEvent(self, event): self.context_menu.emit(event)
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)
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 []
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