class StreamProxy(QtCore.QObject): # only the GUI thread is allowed to write messages in the # LoggerWindow, so this class acts as a proxy, passing messages # over Qt signal/slot for thread safety flush_text = QtSignal() write_text = QtSignal(str) def write(self, msg): msg = msg.strip() if msg: self.write_text.emit(msg) def flush(self): self.flush_text.emit()
class UploaderSession(QtCore.QObject): connection_changed = QtSignal(bool) def __init__(self, *arg, **kwds): super(UploaderSession, self).__init__(*arg, **kwds) self.api = None # get api client id and secret for option in key_store.config.options(self.name): setattr(self, option, key_store.get(self.name, option)) @QtSlot() @catch_all def log_out(self): keyring.delete_password('photini', self.name) self.close_connection() def close_connection(self): self.connection_changed.emit(False) if self.api: self.api.close() self.api = None def get_password(self): return keyring.get_password('photini', self.name) def set_password(self, password): keyring.set_password('photini', self.name, password)
class AuthServer(QtCore.QObject): finished = QtSignal() response = QtSignal(dict) @QtSlot() @catch_all def handle_requests(self): self.server.timeout = 10 self.server.result = None # allow user 5 minutes to finish the process timeout = time.time() + 300 while time.time() < timeout: self.server.handle_request() if self.server.result: self.response.emit(self.server.result) break self.server.server_close() self.finished.emit()
class GoogleUploadConfig(QtWidgets.QWidget): new_set = QtSignal() def __init__(self, *arg, **kw): super(GoogleUploadConfig, self).__init__(*arg, **kw) self.setLayout(QtWidgets.QGridLayout()) self.layout().setContentsMargins(0, 0, 0, 0) # create new set new_set_button = QtWidgets.QPushButton( translate('GooglePhotosTab', 'New album')) new_set_button.clicked.connect(self.new_set) self.layout().addWidget(new_set_button, 2, 1) # list of sets widget sets_group = QtWidgets.QGroupBox( translate('GooglePhotosTab', 'Add to albums')) sets_group.setLayout(QtWidgets.QVBoxLayout()) scrollarea = QtWidgets.QScrollArea() scrollarea.setFrameStyle(QtWidgets.QFrame.NoFrame) scrollarea.setStyleSheet( "QScrollArea { background-color: transparent }") self.sets_widget = QtWidgets.QWidget() self.sets_widget.setLayout(QtWidgets.QVBoxLayout()) self.sets_widget.layout().setSpacing(0) self.sets_widget.layout().setSizeConstraint( QtWidgets.QLayout.SetMinAndMaxSize) scrollarea.setWidget(self.sets_widget) self.sets_widget.setAutoFillBackground(False) sets_group.layout().addWidget(scrollarea) self.layout().addWidget(sets_group, 0, 2, 3, 1) self.layout().setColumnStretch(2, 1) def clear_sets(self): for child in self.sets_widget.children(): if child.isWidgetType(): self.sets_widget.layout().removeWidget(child) child.setParent(None) def checked_albums(self): result = [] for child in self.sets_widget.children(): if child.isWidgetType() and child.isChecked(): result.append(child.property('id')) return result def add_album(self, album, index=-1): widget = QtWidgets.QCheckBox(album['title'].replace('&', '&&')) widget.setProperty('id', album['id']) widget.setEnabled(album['isWriteable']) if index >= 0: self.sets_widget.layout().insertWidget(index, widget) else: self.sets_widget.layout().addWidget(widget) return widget
class DateLink(QtWidgets.QCheckBox): new_link = QtSignal(str) def __init__(self, name, *arg, **kw): super(DateLink, self).__init__(*arg, **kw) self.name = name self.clicked.connect(self._clicked) @QtSlot() @catch_all def _clicked(self): self.new_link.emit(self.name)
class DateAndTimeWidget(QtWidgets.QGridLayout): new_value = QtSignal(str, dict) def __init__(self, name, *arg, **kw): super(DateAndTimeWidget, self).__init__(*arg, **kw) self.name = name self.setVerticalSpacing(0) self.setColumnStretch(3, 1) self.members = {} # date & time self.members['datetime'] = DateTimeEdit() self.addWidget(self.members['datetime'], 0, 0, 1, 2) # time zone self.members['tz_offset'] = TimeZoneWidget() self.addWidget(self.members['tz_offset'], 0, 2) # precision self.addWidget( QtWidgets.QLabel(translate('TechnicalTab', 'Precision:')), 1, 0) self.members['precision'] = PrecisionSlider(Qt.Horizontal) self.members['precision'].setRange(1, 6) self.members['precision'].setValue(6) self.members['precision'].setPageStep(1) self.addWidget(self.members['precision'], 1, 1) # connections self.members['precision'].value_changed.connect( self.members['datetime'].set_precision) self.members['datetime'].editingFinished.connect(self.editing_finished) self.members['tz_offset'].editingFinished.connect(self.editing_finished) self.members['precision'].editing_finished.connect(self.editing_finished) def set_enabled(self, enabled): for widget in self.members.values(): widget.setEnabled(enabled) def get_value(self): new_value = {} for key in self.members: if self.members[key].is_multiple(): continue new_value[key] = self.members[key].get_value() if key == 'datetime' and new_value[key]: if using_pyside: new_value[key] = new_value[key].toPython() else: new_value[key] = new_value[key].toPyDateTime() return new_value @QtSlot() @catch_all def editing_finished(self): self.new_value.emit(self.name, self.get_value())
class UploaderSession(QtCore.QObject): connection_changed = QtSignal(bool) @QtSlot() @catch_all def log_out(self): keyring.delete_password('photini', self.name) self.close_connection() def get_password(self): return keyring.get_password('photini', self.name) def set_password(self, password): keyring.set_password('photini', self.name, password)
class LatLongDisplay(SingleLineEdit): changed = QtSignal() def __init__(self, image_list, *args, **kwds): super(LatLongDisplay, self).__init__(*args, **kwds) self.image_list = image_list self.label = QtWidgets.QLabel(translate('MapTabsAll', 'Lat, long')) self.label.setAlignment(Qt.AlignRight) self.setFixedWidth(width_for_text(self, '8' * 23)) self.setEnabled(False) self.editingFinished.connect(self.editing_finished) @QtSlot() @catch_all def editing_finished(self): selected_images = self.image_list.get_selected_images() new_value = self.get_value().strip() or None if new_value: try: new_value = list(map(float, new_value.split(','))) except Exception: # user typed in an invalid value self.update_display(selected_images) return for image in selected_images: image.metadata.latlong = new_value self.update_display(selected_images) self.changed.emit() def update_display(self, selected_images=None): if selected_images is None: selected_images = self.image_list.get_selected_images() if not selected_images: self.set_value(None) self.setEnabled(False) return values = [] for image in selected_images: value = image.metadata.latlong if value not in values: values.append(value) if len(values) > 1: self.set_multiple(choices=filter(None, values)) else: self.set_value(values[0]) self.setEnabled(True)
class LocationInfo(QtWidgets.QWidget): new_value = QtSignal(object, dict) def __init__(self, *args, **kw): super(LocationInfo, self).__init__(*args, **kw) layout = QtWidgets.QGridLayout() self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self.members = {} for key in ('sublocation', 'city', 'province_state', 'country_name', 'country_code', 'world_region'): self.members[key] = SingleLineEdit() self.members[key].editingFinished.connect(self.editing_finished) self.members['country_code'].setMaximumWidth(40) for j, text in enumerate(( translate('AddressTab', 'Street'), translate('AddressTab', 'City'), translate('AddressTab', 'Province'), translate('AddressTab', 'Country'), translate('AddressTab', 'Region'), )): label = QtWidgets.QLabel(text) label.setAlignment(Qt.AlignRight) layout.addWidget(label, j, 0) layout.addWidget(self.members['sublocation'], 0, 1, 1, 2) layout.addWidget(self.members['city'], 1, 1, 1, 2) layout.addWidget(self.members['province_state'], 2, 1, 1, 2) layout.addWidget(self.members['country_name'], 3, 1) layout.addWidget(self.members['country_code'], 3, 2) layout.addWidget(self.members['world_region'], 4, 1, 1, 2) layout.setRowStretch(5, 1) def get_value(self): new_value = {} for key in self.members: if self.members[key].is_multiple(): continue new_value[key] = self.members[key].get_value().strip() or None return new_value @QtSlot() @catch_all def editing_finished(self): self.new_value.emit(self, self.get_value())
class NameMangler(QtCore.QObject): number_parser = re.compile(r'(\d+)') new_example = QtSignal(str) def __init__(self, parent=None): super(NameMangler, self).__init__(parent) self.example = None self.format_string = None @QtSlot(str) @catch_all def new_format(self, format_string): self.format_string = format_string self.refresh_example() def set_example(self, example): self.example = example self.refresh_example() def refresh_example(self): if self.format_string and self.example: self.new_example.emit(self.transform(self.example)) def transform(self, file_data): name = file_data['name'] subst = {'name': name} numbers = self.number_parser.findall(name) if numbers: subst['number'] = numbers[-1] else: subst['number'] = '' subst['root'], subst['ext'] = os.path.splitext(name) subst['camera'] = file_data['camera'] or 'unknown_camera' subst['camera'] = subst['camera'].replace(' ', '_') # process {...} parts first try: result = self.format_string.format(**subst) except (KeyError, ValueError): result = self.format_string # then do timestamp return file_data['timestamp'].strftime(result)
class MapWebView(QWebView): drop_text = QtSignal(int, int, str) @catch_all def dragEnterEvent(self, event): if not event.mimeData().hasFormat(DRAG_MIMETYPE): return super(MapWebView, self).dragEnterEvent(event) event.acceptProposedAction() @catch_all def dragMoveEvent(self, event): if not event.mimeData().hasFormat(DRAG_MIMETYPE): return super(MapWebView, self).dragMoveEvent(event) @catch_all def dropEvent(self, event): if not event.mimeData().hasFormat(DRAG_MIMETYPE): return super(MapWebView, self).dropEvent(event) text = event.mimeData().data(DRAG_MIMETYPE).data().decode('utf-8') if text: self.drop_text.emit(event.pos().x(), event.pos().y(), text)
class PrecisionSlider(Slider): value_changed = QtSignal(int) def __init__(self, *arg, **kw): super(PrecisionSlider, self).__init__(*arg, **kw) self.valueChanged.connect(self._value_changed) def _value_changed(self, value): if value >= 4: value += 1 self.value_changed.emit(value) def get_value(self): value = super(PrecisionSlider, self).get_value() if value >= 4: value += 1 return value def set_value(self, value): if value is not None and value >= 5: value -= 1 super(PrecisionSlider, self).set_value(value)
class ServerSocket(QtCore.QObject): new_files = QtSignal(list) def __init__(self, socket, *arg, **kw): super(ServerSocket, self).__init__(*arg, **kw) self.socket = socket self.data = b'' self.socket.setParent(self) self.socket.readyRead.connect(self.read_data) self.socket.disconnected.connect(self.deleteLater) @QtSlot() @catch_all def read_data(self): file_list = [] while self.socket.bytesAvailable(): self.data += self.socket.readAll().data() while b'\n' in self.data: line, sep, self.data = self.data.partition(b'\n') string = line.decode('utf-8') file_list.append(string) if file_list: self.new_files.emit(file_list)
class InstanceServer(QtNetwork.QTcpServer): new_files = QtSignal(list) def __init__(self, *arg, **kw): super(InstanceServer, self).__init__(*arg, **kw) config = BaseConfigStore('instance') self.newConnection.connect(self.new_connection) if not self.listen(QtNetwork.QHostAddress.LocalHost): logger.error('Failed to start instance server:', self.errorString()) return config.set('server', 'port', self.serverPort()) config.save() @QtSlot() @catch_all def new_connection(self): window = self.parent().window() window.raise_() window.activateWindow() while self.hasPendingConnections(): socket = self.nextPendingConnection() socket = ServerSocket(socket, parent=self) socket.new_files.connect(self.new_files)
class QTabBar(QtWidgets.QTabBar): context_menu = QtSignal(QtGui.QContextMenuEvent) @catch_all def contextMenuEvent(self, event): self.context_menu.emit(event)
class OffsetWidget(QtWidgets.QWidget): apply_offset = QtSignal(timedelta, object) def __init__(self, *arg, **kw): super(OffsetWidget, self).__init__(*arg, **kw) self.config_store = QtWidgets.QApplication.instance().config_store self.setLayout(QtWidgets.QHBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) spacing = self.layout().spacing() self.layout().setSpacing(0) # offset value self.offset = QtWidgets.QTimeEdit() self.offset.setDisplayFormat("'h:'hh 'm:'mm 's:'ss") self.layout().addWidget(self.offset) self.layout().addSpacing(spacing) # time zone self.time_zone = TimeZoneWidget() self.time_zone.set_value(None) self.layout().addWidget(self.time_zone) self.layout().addSpacing(spacing) # add offset button add_button = QtWidgets.QPushButton(chr(0x002b)) add_button.setStyleSheet('QPushButton {padding: 0px}') set_symbol_font(add_button) scale_font(add_button, 170) add_button.setFixedWidth(self.offset.sizeHint().height()) add_button.setFixedHeight(self.offset.sizeHint().height()) add_button.clicked.connect(self.add) self.layout().addWidget(add_button) # subtract offset button sub_button = QtWidgets.QPushButton(chr(0x2212)) sub_button.setStyleSheet('QPushButton {padding: 0px}') set_symbol_font(sub_button) scale_font(sub_button, 170) sub_button.setFixedWidth(self.offset.sizeHint().height()) sub_button.setFixedHeight(self.offset.sizeHint().height()) sub_button.clicked.connect(self.sub) self.layout().addWidget(sub_button) self.layout().addStretch(1) # restore stored values value = self.config_store.get('technical', 'offset') if value: self.offset.setTime(QtCore.QTime(*value[0:3])) self.time_zone.set_value(value[3]) # connections self.offset.editingFinished.connect(self.new_value) self.time_zone.editingFinished.connect(self.new_value) @catch_all def showEvent(self, event): super(OffsetWidget, self).showEvent(event) # On some Windows versions the initial sizeHint calculation is # wrong. Redoing it after the widget becomes visible gets a # better result. Calling setSpecialValueText is also required. self.offset.setSpecialValueText('') self.offset.updateGeometry() self.time_zone.setSpecialValueText(' ') self.time_zone.updateGeometry() @QtSlot() @catch_all def new_value(self): value = self.offset.time() value = (value.hour(), value.minute(), value.second(), self.time_zone.get_value()) self.config_store.set('technical', 'offset', value) @QtSlot() @catch_all def add(self): self.do_inc(False) @QtSlot() @catch_all def sub(self): self.do_inc(True) def do_inc(self, negative): value = self.offset.time() offset = timedelta( hours=value.hour(), minutes=value.minute(), seconds=value.second()) tz_offset = self.time_zone.get_value() if negative: if tz_offset is not None: tz_offset = -tz_offset offset = -offset self.apply_offset.emit(offset, tz_offset)
class PhotiniUploader(QtWidgets.QWidget): abort_upload = QtSignal(bool) def __init__(self, upload_config_widget, image_list, *arg, **kw): super(PhotiniUploader, self).__init__(*arg, **kw) self.app = QtWidgets.QApplication.instance() self.app.aboutToQuit.connect(self.shutdown) logger.debug('using %s', keyring.get_keyring().__module__) self.image_list = image_list self.setLayout(QtWidgets.QGridLayout()) self.session = self.session_factory() self.session.connection_changed.connect(self.connection_changed) self.upload_worker = None # user details self.user = {} user_group = QtWidgets.QGroupBox(translate('UploaderTabsAll', 'User')) user_group.setLayout(QtWidgets.QVBoxLayout()) self.user_photo = QtWidgets.QLabel() self.user_photo.setAlignment(Qt.AlignHCenter | Qt.AlignTop) user_group.layout().addWidget(self.user_photo) self.user_name = QtWidgets.QLabel() self.user_name.setWordWrap(True) self.user_name.setFixedWidth(80) user_group.layout().addWidget(self.user_name) user_group.layout().addStretch(1) self.layout().addWidget(user_group, 0, 0, 1, 2) # connect / disconnect button self.user_connect = StartStopButton( translate('UploaderTabsAll', 'Log in'), translate('UploaderTabsAll', 'Log out')) self.user_connect.click_start.connect(self.log_in) self.user_connect.click_stop.connect(self.session.log_out) self.layout().addWidget(self.user_connect, 1, 0, 1, 2) # 'service' specific widget self.layout().addWidget(upload_config_widget, 0, 2, 2, 2) # upload button self.upload_button = StartStopButton( translate('UploaderTabsAll', 'Start upload'), translate('UploaderTabsAll', 'Stop upload')) self.upload_button.setEnabled(False) self.upload_button.click_start.connect(self.start_upload) self.upload_button.click_stop.connect(self.stop_upload) self.layout().addWidget(self.upload_button, 2, 3) # progress bar self.layout().addWidget( QtWidgets.QLabel(translate('UploaderTabsAll', 'Progress')), 2, 0) self.total_progress = QtWidgets.QProgressBar() self.layout().addWidget(self.total_progress, 2, 1, 1, 2) # adjust spacing self.layout().setColumnStretch(2, 1) self.layout().setRowStretch(0, 1) # initialise as not connected self.connection_changed(False) def tr(self, *arg, **kw): return QtCore.QCoreApplication.translate('UploaderTabsAll', *arg, **kw) @QtSlot() @catch_all def shutdown(self): self.session.close_connection() @QtSlot(bool) @catch_all def connection_changed(self, connected): if connected: with Busy(): self.show_user(*self.session.get_user()) self.show_album_list(self.session.get_albums()) else: self.show_user(None, None) self.show_album_list([]) self.user_connect.set_checked(connected) self.upload_config.setEnabled(connected and not self.upload_worker) self.user_connect.setEnabled(not self.upload_worker) self.enable_upload_button() def refresh(self): if not self.user_connect.is_checked(): self.log_in(do_auth=False) self.enable_upload_button() def do_not_close(self): if not self.upload_worker: return False dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle(translate( 'UploaderTabsAll', 'Photini: upload in progress')) dialog.setText(translate( 'UploaderTabsAll', '<h3>Upload to {} has not finished.</h3>').format(self.service_name)) dialog.setInformativeText( translate('UploaderTabsAll', 'Closing now will terminate the upload.')) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons( QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Cancel) dialog.setDefaultButton(QtWidgets.QMessageBox.Cancel) result = dialog.exec_() return result == QtWidgets.QMessageBox.Cancel def show_user(self, name, picture): if name: self.user_name.setText(translate( 'UploaderTabsAll', 'Logged in as {0} on {1}').format(name, self.service_name)) else: self.user_name.setText(translate( 'UploaderTabsAll', 'Not logged in to {}').format(self.service_name)) pixmap = QtGui.QPixmap() if picture: pixmap.loadFromData(picture) self.user_photo.setPixmap(pixmap) def get_temp_filename(self, image, ext='.jpg'): temp_dir = appdirs.user_cache_dir('photini') if not os.path.isdir(temp_dir): os.makedirs(temp_dir) return os.path.join(temp_dir, os.path.basename(image.path) + ext) def copy_metadata(self, image, path): # copy metadata md = Metadata.clone(path, image.metadata) # save metedata, forcing IPTC creation md.dirty = True md.save(if_mode=True, sc_mode='none', force_iptc=True) def convert_to_jpeg(self, image): im = QtGui.QImage(image.path) path = self.get_temp_filename(image) im.save(path, format='jpeg', quality=95) self.copy_metadata(image, path) return path def copy_file_and_metadata(self, image): path = self.get_temp_filename(image, ext='') shutil.copyfile(image.path, path) self.copy_metadata(image, path) return path def is_convertible(self, image): if not image.file_type.startswith('image'): # can only convert images return False return QtGui.QImageReader(image.path).canRead() def get_conversion_function(self, image, params): if image.file_type in self.image_types['accepted']: if image.file_type.startswith('video'): # don't try to write metadata to videos return None if image.metadata.find_sidecar() or not image.metadata.iptc_in_file: # need to create file without sidecar and with IPTC return self.copy_file_and_metadata return None if not self.is_convertible(image): msg = translate( 'UploaderTabsAll', 'File "{0}" is of type "{1}", which {2} does not' + ' accept and Photini cannot convert.') buttons = QtWidgets.QMessageBox.Ignore elif (self.image_types['rejected'] == '*' or image.file_type in self.image_types['rejected']): msg = translate( 'UploaderTabsAll', 'File "{0}" is of type "{1}", which {2} does not' + ' accept. Would you like to convert it to JPEG?') buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Ignore else: msg = translate( 'UploaderTabsAll', 'File "{0}" is of type "{1}", which {2} may not' + ' handle correctly. Would you like to convert it to JPEG?') buttons = QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle( translate('UploaderTabsAll', 'Photini: incompatible type')) dialog.setText( translate('UploaderTabsAll', '<h3>Incompatible image type.</h3>')) dialog.setInformativeText(msg.format(os.path.basename(image.path), image.file_type, self.service_name)) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(buttons) dialog.setDefaultButton(QtWidgets.QMessageBox.Yes) result = dialog.exec_() if result == QtWidgets.QMessageBox.Ignore: return 'omit' if result == QtWidgets.QMessageBox.Yes: return self.convert_to_jpeg return None @QtSlot() @catch_all def stop_upload(self): self.abort_upload.emit(False) @QtSlot() @catch_all def start_upload(self): if not self.image_list.unsaved_files_dialog(with_discard=False): return # make list of items to upload upload_list = [] for image in self.image_list.get_selected_images(): params = self.get_upload_params(image) if not params: continue convert = self.get_conversion_function(image, params) if convert == 'omit': continue upload_list.append((image, convert, params)) if not upload_list: self.upload_button.setChecked(False) return self.upload_button.set_checked(True) self.upload_config.setEnabled(False) self.user_connect.setEnabled(False) # do uploading in separate thread, so GUI can continue self.upload_worker = UploadWorker(self.session_factory, upload_list) thread = QtCore.QThread(self) self.upload_worker.moveToThread(thread) self.upload_worker.upload_error.connect( self.upload_error, Qt.BlockingQueuedConnection) self.abort_upload.connect( self.upload_worker.abort_upload, Qt.DirectConnection) self.upload_worker.upload_progress.connect(self.upload_progress) thread.started.connect(self.upload_worker.start) self.upload_worker.finished.connect(self.uploader_finished) self.upload_worker.finished.connect(thread.quit) self.upload_worker.finished.connect(self.upload_worker.deleteLater) thread.finished.connect(thread.deleteLater) thread.start() @QtSlot(float, str) @catch_all def upload_progress(self, value, format_): self.total_progress.setValue(value) if format_: self.total_progress.setFormat(format_) @QtSlot(str, str) @catch_all def upload_error(self, name, error): dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle(translate( 'UploaderTabsAll', 'Photini: upload error')) dialog.setText(translate( 'UploaderTabsAll', '<h3>File "{}" upload failed.</h3>').format( name)) dialog.setInformativeText(error) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStandardButtons(QtWidgets.QMessageBox.Abort | QtWidgets.QMessageBox.Retry) dialog.setDefaultButton(QtWidgets.QMessageBox.Retry) self.abort_upload.emit(dialog.exec_() == QtWidgets.QMessageBox.Retry) @QtSlot() @catch_all def uploader_finished(self): self.upload_button.set_checked(False) self.upload_config.setEnabled(True) self.user_connect.setEnabled(True) self.upload_worker = None self.enable_upload_button() @QtSlot() @catch_all def log_in(self, do_auth=True): with DisableWidget(self.user_connect): with Busy(): connect = self.session.open_connection() if connect is None: # can't reach server return if do_auth and not connect: self.authorise() def authorise(self): with Busy(): # do full authentication procedure http_server = HTTPServer(('127.0.0.1', 0), AuthRequestHandler) redirect_uri = 'http://127.0.0.1:' + str(http_server.server_port) auth_url = self.session.get_auth_url(redirect_uri) if not auth_url: logger.error('Failed to get auth URL') http_server.server_close() return server = AuthServer() thread = QtCore.QThread(self) server.moveToThread(thread) server.server = http_server server.response.connect(self.auth_response) thread.started.connect(server.handle_requests) server.finished.connect(thread.quit) server.finished.connect(server.deleteLater) thread.finished.connect(thread.deleteLater) thread.start() if QtGui.QDesktopServices.openUrl(QtCore.QUrl(auth_url)): return logger.error('Failed to open web browser') @QtSlot(dict) @catch_all def auth_response(self, result): with Busy(): self.session.get_access_token(result) @QtSlot(list) @catch_all def new_selection(self, selection): self.enable_upload_button(selection=selection) def enable_upload_button(self, selection=None): if self.upload_button.is_checked(): # can always cancel upload in progress self.upload_button.setEnabled(True) return if not self.user_connect.is_checked(): # can't upload if not logged in self.upload_button.setEnabled(False) return if selection is None: selection = self.image_list.get_selected_images() self.upload_button.setEnabled(len(selection) > 0)
class AugmentSpinBox(object): new_value = QtSignal(object) def init_augment(self): self.set_value(None) self.editingFinished.connect(self.editing_finished) class ContextAction(QtGui2.QAction): def __init__(self, value, *arg, **kw): super(AugmentSpinBox.ContextAction, self).__init__(*arg, **kw) self.setData(value) self.triggered.connect(self.set_value) @QtSlot() @catch_all def set_value(self): self.parent().setValue(self.data()) def context_menu_event(self): if self.specialValueText() and self.choices: QtCore.QTimer.singleShot(0, self.extend_context_menu) @QtSlot() @catch_all def extend_context_menu(self): menu = self.findChild(QtWidgets.QMenu) if not menu: return sep = menu.insertSeparator(menu.actions()[0]) for suggestion in self.choices: menu.insertAction(sep, self.ContextAction( suggestion, text=self.textFromValue(suggestion), parent=self)) def clear_special_value(self): if self.specialValueText(): self.set_value(self.default_value) self.selectAll() @QtSlot() @catch_all def editing_finished(self): if self.is_multiple(): return self.get_value(emit=True) def get_value(self, emit=False): value = self.value() if value == self.minimum() and self.specialValueText(): value = None if emit: self.new_value.emit(value) return value def set_value(self, value): if value is None: self.setValue(self.minimum()) self.setSpecialValueText(' ') else: self.setSpecialValueText('') self.setValue(value) def set_multiple(self, choices=[]): self.choices = list(filter(None, choices)) self.setValue(self.minimum()) self.setSpecialValueText(self.multiple) def is_multiple(self): return (self.value() == self.minimum() and self.specialValueText() == self.multiple)
class DropdownEdit(ComboBox): extend_list = QtSignal() new_value = QtSignal(object) def __init__(self, extendable=False, **kw): super(DropdownEdit, self).__init__(**kw) if extendable: self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.remove_from_list) self.addItem( translate('TechnicalTab', '<new>'), self.extend_list.emit) self.addItem('', None) self.first_value_idx = self.count() self.addItem(multiple_values(), '<multiple>') self.setItemData( self.count() - 1, self.itemData(self.count() - 1), Qt.UserRole - 1) self.currentIndexChanged.connect(self.current_index_changed) @QtSlot(QtCore.QPoint) @catch_all def remove_from_list(self, pos): current_value = self.itemData(self.currentIndex()) menu = QtWidgets.QMenu() for name, value in self.get_items(): if value == current_value: continue action = QtGui2.QAction( translate('TechnicalTab', 'Remove "{}"').format(name), parent=self) action.setData(value) menu.addAction(action) if menu.isEmpty(): return action = menu.exec_(self.mapToGlobal(pos)) if not action: return self.remove_item(action.data()) @QtSlot(int) @catch_all def current_index_changed(self, idx): value = self.itemData(idx) if callable(value): (value)() else: self.new_value.emit(value) def add_item(self, text, value, ordered=True): blocked = self.blockSignals(True) position = self.count() - 1 if ordered: for n in range(self.first_value_idx, self.count() - 1): if self.itemText(n).lower() > text.lower(): position = n break self.insertItem(position, text, value) self.set_dropdown_width() self.blockSignals(blocked) def remove_item(self, value): blocked = self.blockSignals(True) self.removeItem(self.find_data(value)) self.set_dropdown_width() self.blockSignals(blocked) def known_value(self, value): if not value: return True return self.find_data(value) >= 0 def set_value(self, value): blocked = self.blockSignals(True) self.setCurrentIndex(self.find_data(value)) self.blockSignals(blocked) def find_data(self, value): # Qt's findData only works for simple types for n in range(self.count()): if self.itemData(n) == value: return n return -1 def get_items(self): for n in range(self.first_value_idx, self.count() - 1): yield self.itemText(n), self.itemData(n) def set_multiple(self): blocked = self.blockSignals(True) self.setCurrentIndex(self.count() - 1) self.blockSignals(blocked)
class UploadWorker(QtCore.QObject): finished = QtSignal() upload_error = QtSignal(str, str) upload_progress = QtSignal(float, str) def __init__(self, session_factory, upload_list, *args, **kwds): super(UploadWorker, self).__init__(*args, **kwds) self.session_factory = session_factory self.upload_list = upload_list self.fileobj = None @QtSlot() @catch_all def start(self): session = self.session_factory() session.open_connection() upload_count = 0 while upload_count < len(self.upload_list): image, convert, params = self.upload_list[upload_count] name = os.path.basename(image.path) self.upload_progress.emit(0.0, '{} ({}/{}) %p%'.format( name, 1 + upload_count, len(self.upload_list))) if convert: path = convert(image) else: path = image.path with open(path, 'rb') as f: self.fileobj = FileObjWithCallback(f, self.progress) try: error = session.do_upload( self.fileobj, imghdr.what(path), image, params) except UploadAborted: break except Exception as ex: error = str(ex) self.fileobj = None if convert: os.unlink(path) if error: self.retry = None self.upload_error.emit(name, error) # wait for response from user dialog while self.retry is None: QtWidgets.QApplication.processEvents() if not self.retry: break else: upload_count += 1 self.upload_progress.emit(0.0, '%p%') session.close_connection() self.finished.emit() def progress(self, value): self.upload_progress.emit(value, '') @QtSlot(bool) @catch_all def abort_upload(self, retry): self.retry = retry if self.fileobj: # brutal way to interrupt an upload self.fileobj.abort()
class FlickrUploadConfig(QtWidgets.QWidget): new_set = QtSignal() sync_metadata = QtSignal() def __init__(self, *arg, **kw): super(FlickrUploadConfig, self).__init__(*arg, **kw) self.setLayout(QtWidgets.QGridLayout()) self.layout().setContentsMargins(0, 0, 0, 0) # privacy settings self.privacy = {} privacy_group = QtWidgets.QGroupBox( translate('FlickrTab', 'Who can see the photos?')) privacy_group.setLayout(QtWidgets.QVBoxLayout()) self.privacy['private'] = QtWidgets.QRadioButton( translate('FlickrTab', 'Only you')) privacy_group.layout().addWidget(self.privacy['private']) ff_group = QtWidgets.QGroupBox() ff_group.setFlat(True) ff_group.setLayout(QtWidgets.QVBoxLayout()) ff_group.layout().setContentsMargins(10, 0, 0, 0) self.privacy['friends'] = QtWidgets.QCheckBox( translate('FlickrTab', 'Your friends')) ff_group.layout().addWidget(self.privacy['friends']) self.privacy['family'] = QtWidgets.QCheckBox( translate('FlickrTab', 'Your family')) ff_group.layout().addWidget(self.privacy['family']) privacy_group.layout().addWidget(ff_group) self.privacy['public'] = QtWidgets.QRadioButton( translate('FlickrTab', 'Anyone')) self.privacy['public'].toggled.connect(self.enable_ff) self.privacy['public'].setChecked(True) privacy_group.layout().addWidget(self.privacy['public']) self.hidden = QtWidgets.QCheckBox( translate('FlickrTab', 'Hidden from search')) privacy_group.layout().addWidget(self.hidden) privacy_group.layout().addStretch(1) self.layout().addWidget(privacy_group, 0, 0, 3, 1) # content type self.content_type = {} content_group = QtWidgets.QGroupBox( translate('FlickrTab', 'Content type')) content_group.setLayout(QtWidgets.QVBoxLayout()) self.content_type['photo'] = QtWidgets.QRadioButton( translate('FlickrTab', 'Photo')) self.content_type['photo'].setChecked(True) content_group.layout().addWidget(self.content_type['photo']) self.content_type['screenshot'] = QtWidgets.QRadioButton( translate('FlickrTab', 'Screenshot')) content_group.layout().addWidget(self.content_type['screenshot']) self.content_type['other'] = QtWidgets.QRadioButton( translate('FlickrTab', 'Art/Illustration')) content_group.layout().addWidget(self.content_type['other']) content_group.layout().addStretch(1) self.layout().addWidget(content_group, 0, 1) # synchronise metadata self.sync_button = QtWidgets.QPushButton( translate('FlickrTab', 'Synchronise')) self.sync_button.clicked.connect(self.sync_metadata) self.layout().addWidget(self.sync_button, 1, 1) # create new set new_set_button = QtWidgets.QPushButton( translate('FlickrTab', 'New album')) new_set_button.clicked.connect(self.new_set) self.layout().addWidget(new_set_button, 2, 1) # list of sets widget sets_group = QtWidgets.QGroupBox( translate('FlickrTab', 'Add to albums')) sets_group.setLayout(QtWidgets.QVBoxLayout()) scrollarea = QtWidgets.QScrollArea() scrollarea.setFrameStyle(QtWidgets.QFrame.NoFrame) scrollarea.setStyleSheet("QScrollArea { background-color: transparent }") self.sets_widget = QtWidgets.QWidget() self.sets_widget.setLayout(QtWidgets.QVBoxLayout()) self.sets_widget.layout().setSpacing(0) self.sets_widget.layout().setSizeConstraint( QtWidgets.QLayout.SetMinAndMaxSize) scrollarea.setWidget(self.sets_widget) self.sets_widget.setAutoFillBackground(False) sets_group.layout().addWidget(scrollarea) self.layout().addWidget(sets_group, 0, 2, 3, 1) self.layout().setColumnStretch(2, 1) @QtSlot(bool) @catch_all def enable_ff(self, value): self.privacy['friends'].setEnabled(self.privacy['private'].isChecked()) self.privacy['family'].setEnabled(self.privacy['private'].isChecked()) def get_fixed_params(self): is_public = str(int(self.privacy['public'].isChecked())) is_family = str(int(self.privacy['private'].isChecked() and self.privacy['family'].isChecked())) is_friend = str(int(self.privacy['private'].isChecked() and self.privacy['friends'].isChecked())) if self.content_type['photo'].isChecked(): content_type = '1' elif self.content_type['screenshot'].isChecked(): content_type = '2' else: content_type = '3' hidden = str(int(self.hidden.isChecked())) return { 'permissions': { 'is_public': is_public, 'is_friend': is_friend, 'is_family': is_family, }, 'content_type': {'content_type': content_type}, 'hidden' : {'hidden' : hidden}, } def clear_sets(self): for child in self.sets_widget.children(): if child.isWidgetType(): self.sets_widget.layout().removeWidget(child) child.setParent(None) def checked_sets(self): result = [] for child in self.sets_widget.children(): if child.isWidgetType() and child.isChecked(): result.append(child) return result def add_set(self, title, description, photoset_id, index=-1): widget = QtWidgets.QCheckBox(title.replace('&', '&&')) if description: widget.setToolTip(html.unescape(description)) widget.setProperty('photoset_id', photoset_id) if index >= 0: self.sets_widget.layout().insertWidget(index, widget) else: self.sets_widget.layout().addWidget(widget) return widget
class SpellCheck(QtCore.QObject): new_dict = QtSignal() def __init__(self, *arg, **kw): super(SpellCheck, self).__init__(*arg, **kw) self.config_store = QtWidgets.QApplication.instance().config_store self.enable(self.config_store.get('spelling', 'enabled', True)) self.set_language(self.config_store.get('spelling', 'language')) @staticmethod def available_languages(): result = defaultdict(list) if Gspell: for lang in Gspell.Language.get_available(): code = lang.get_code() name = lang.get_name() match = re.match('(.+)\s+\((.+?)\)', name) if match: language = match.group(1) country = match.group(2) if country == 'any': country = '' else: language = name country = '' result[language].append((country, code)) elif enchant: for code in enchant.list_languages(): locale = QtCore.QLocale(code) language = locale.languageToString(locale.language()) if '_' in code and '_ANY' not in code: country = locale.countryToString(locale.country()) else: country = '' result[language].append((country, code)) else: return None for value in result.values(): value.sort() return dict(result) or None def current_language(self): if not self.dict: return '' if Gspell: language = self.dict.get_language() if language: return language.get_code() elif enchant: return self.dict.tag return '' @QtSlot(bool) @catch_all def enable(self, enabled): self.config_store.set('spelling', 'enabled', enabled) self.enabled = enabled and bool(Gspell or enchant) self.new_dict.emit() def set_language(self, code): if code: logger.debug('Setting dictionary %s', code) self.dict = None if Gspell: if code: self.dict = Gspell.Checker.new(Gspell.Language.lookup(code)) elif enchant: if code and enchant.dict_exists(code): self.dict = enchant.Dict(code) else: return if code and not self.dict: logger.warning('Failed to set dictionary %s', code) self.config_store.set('spelling', 'language', self.current_language()) self.new_dict.emit() words = re.compile(r"\w+([-'’]\w+)*", flags=re.IGNORECASE | re.UNICODE) def find_words(self, text): for word in self.words.finditer(text): yield word.group(), word.start(), word.end() def check(self, word): if not (word and self.enabled and self.dict): return True if word.isnumeric(): return True if Gspell: return self.dict.check_word(word, -1) if enchant: return self.dict.check(word) return True def suggest(self, word): if self.check(word): return [] if Gspell: return GSListPtr_to_list(self.dict.get_suggestions(word, -1)) if enchant: return self.dict.suggest(word) return []
class SpellCheck(QtCore.QObject): new_dict = QtSignal() def __init__(self, *arg, **kw): super(SpellCheck, self).__init__(*arg, **kw) self.config_store = QtWidgets.QApplication.instance().config_store self.enable(eval(self.config_store.get('spelling', 'enabled', 'True'))) self.set_language(self.config_store.get('spelling', 'language')) @staticmethod def available_languages(): result = [] if Gspell: for lang in Gspell.Language.get_available(): result.append((lang.get_name(), lang.get_code())) elif enchant: languages = enchant.list_languages() languages.sort() for lang in languages: result.append((lang, lang)) else: return None return result def current_language(self): if not self.dict: return '' if Gspell: language = self.dict.get_language() if language: return language.get_code() elif enchant: return self.dict.tag return '' @QtSlot(bool) @catch_all def enable(self, enabled): self.enabled = enabled and bool(Gspell or enchant) self.config_store.set('spelling', 'enabled', str(self.enabled)) self.new_dict.emit() def set_language(self, code): if code: logger.debug('Setting dictionary %s', code) self.dict = None if Gspell: if code: self.dict = Gspell.Checker.new(Gspell.Language.lookup(code)) elif enchant: if code and enchant.dict_exists(code): self.dict = enchant.Dict(code) if code and not self.dict: logger.warning('Failed to set dictionary %s', code) self.config_store.set('spelling', 'language', self.current_language()) self.new_dict.emit() words = re.compile(r"\w+([-'’]\w+)*", flags=re.IGNORECASE | re.UNICODE) def find_words(self, text): for word in self.words.finditer(text): yield word.group(), word.start(), word.end() def check(self, word): if not (word and self.enabled and self.dict): return True if Gspell: return self.dict.check_word(word, -1) if enchant: return self.dict.check(word) return True def suggest(self, word): if self.check(word): return [] if Gspell: return GSListPtr_to_list(self.dict.get_suggestions(word, -1)) if enchant: return self.dict.suggest(word) return []
class LocationInfo(QtWidgets.QWidget): new_value = QtSignal(object, dict) def __init__(self, *args, **kw): super(LocationInfo, self).__init__(*args, **kw) layout = QtWidgets.QGridLayout() self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) self.members = {} for key in ('SubLocation', 'City', 'ProvinceState', 'CountryName', 'CountryCode', 'WorldRegion'): self.members[key] = SingleLineEdit( length_check=ImageMetadata.max_bytes(key)) self.members[key].editingFinished.connect(self.editing_finished) self.members['CountryCode'].setMaximumWidth( width_for_text(self.members['CountryCode'], 'W' * 4)) self.members['SubLocation'].setToolTip( translate('AddressTab', 'Enter the name of the sublocation.')) self.members['City'].setToolTip( translate('AddressTab', 'Enter the name of the city.')) self.members['ProvinceState'].setToolTip( translate('AddressTab', 'Enter the name of the province or state.')) self.members['CountryName'].setToolTip( translate('AddressTab', 'Enter the name of the country.')) self.members['CountryCode'].setToolTip( translate( 'AddressTab', 'Enter the 2 or 3 letter ISO 3166 country code of the country.' )) self.members['WorldRegion'].setToolTip( translate('AddressTab', 'Enter the name of the world region.')) for j, text in enumerate(( translate('AddressTab', 'Street'), translate('AddressTab', 'City'), translate('AddressTab', 'Province'), translate('AddressTab', 'Country'), translate('AddressTab', 'Region'), )): label = QtWidgets.QLabel(text) label.setAlignment(Qt.AlignRight) layout.addWidget(label, j, 0) layout.addWidget(self.members['SubLocation'], 0, 1, 1, 2) layout.addWidget(self.members['City'], 1, 1, 1, 2) layout.addWidget(self.members['ProvinceState'], 2, 1, 1, 2) layout.addWidget(self.members['CountryName'], 3, 1) layout.addWidget(self.members['CountryCode'], 3, 2) layout.addWidget(self.members['WorldRegion'], 4, 1, 1, 2) layout.setRowStretch(5, 1) def get_value(self): new_value = {} for key in self.members: if self.members[key].is_multiple(): continue new_value[key] = self.members[key].get_value().strip() or None return new_value @QtSlot() @catch_all def editing_finished(self): self.new_value.emit(self, self.get_value())
class ScrollArea(QtWidgets.QScrollArea): dropped_images = QtSignal(list) multi_row_changed = QtSignal() def __init__(self, parent=None): super(ScrollArea, self).__init__(parent) self.multi_row = None self.set_multi_row(True) self.setWidgetResizable(True) self.setAcceptDrops(True) widget = QtWidgets.QWidget() self.thumbs = ThumbsLayout(scroll_area=self) widget.setLayout(self.thumbs) self.setWidget(widget) # adopt some layout methods self.add_widget = self.thumbs.addWidget self.remove_widget = self.thumbs.removeWidget def set_multi_row(self, multi_row): if multi_row: self.setMinimumHeight(0) else: scrollbar = self.horizontalScrollBar() self.setMinimumHeight(self.thumbs.sizeHint().height() + scrollbar.height()) if multi_row == self.multi_row: return self.multi_row = multi_row if multi_row: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) else: self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.multi_row_changed.emit() def ensureWidgetVisible(self, widget): left, top, right, bottom = self.thumbs.getContentsMargins() super(ScrollArea, self).ensureWidgetVisible(widget, max(left, right), max(top, bottom)) @catch_all def dropEvent(self, event): file_list = [] for uri in event.mimeData().urls(): file_list.append(uri.toLocalFile()) if file_list: self.dropped_images.emit(file_list) @catch_all def dragEnterEvent(self, event): if event.mimeData().hasFormat('text/uri-list'): event.acceptProposedAction() @catch_all def resizeEvent(self, event): super(ScrollArea, self).resizeEvent(event) width = event.size().width() height = event.size().height() if not self.multi_row: scrollbar = self.verticalScrollBar() width -= scrollbar.width() scrollbar = self.horizontalScrollBar() if not scrollbar.isVisible(): height -= scrollbar.height() self.thumbs.set_viewport_size(QtCore.QSize(width, height))
class ImageList(QtWidgets.QWidget): image_list_changed = QtSignal() new_metadata = QtSignal(bool) selection_changed = QtSignal(list) sort_order_changed = QtSignal() def __init__(self, parent=None): super(ImageList, self).__init__(parent) self.app = QtWidgets.QApplication.instance() self.drag_icon = None self.images = [] self.last_selected = None self.selection_anchor = None self.thumb_size = int( self.app.config_store.get('controls', 'thumb_size', '80')) layout = QtWidgets.QGridLayout() layout.setSpacing(0) layout.setRowStretch(0, 1) layout.setColumnStretch(3, 1) self.setLayout(layout) layout.setContentsMargins(0, 0, 0, 0) # thumbnail display self.scroll_area = ScrollArea() self.scroll_area.dropped_images.connect(self.open_file_list) self.scroll_area.multi_row_changed.connect( self._ensure_selected_visible) layout.addWidget(self.scroll_area, 0, 0, 1, 6) QtWidgets.QShortcut(QtGui.QKeySequence.MoveToPreviousChar, self.scroll_area, self.move_to_prev_thumb) QtWidgets.QShortcut(QtGui.QKeySequence.MoveToNextChar, self.scroll_area, self.move_to_next_thumb) QtWidgets.QShortcut(QtGui.QKeySequence.MoveToStartOfLine, self.scroll_area, self.move_to_first_thumb) QtWidgets.QShortcut(QtGui.QKeySequence.MoveToEndOfLine, self.scroll_area, self.move_to_last_thumb) QtWidgets.QShortcut(QtGui.QKeySequence.SelectPreviousChar, self.scroll_area, self.select_prev_thumb) QtWidgets.QShortcut(QtGui.QKeySequence.SelectNextChar, self.scroll_area, self.select_next_thumb) QtWidgets.QShortcut(QtGui.QKeySequence.SelectAll, self.scroll_area, self.select_all) # sort key selector layout.addWidget(QtWidgets.QLabel(self.tr('sort by: ')), 1, 0) self.sort_name = QtWidgets.QRadioButton(self.tr('file name')) self.sort_name.clicked.connect(self._new_sort_order) layout.addWidget(self.sort_name, 1, 1) self.sort_date = QtWidgets.QRadioButton(self.tr('date taken')) layout.addWidget(self.sort_date, 1, 2) self.sort_date.clicked.connect(self._new_sort_order) if eval(self.app.config_store.get('controls', 'sort_date', 'False')): self.sort_date.setChecked(True) else: self.sort_name.setChecked(True) # size selector layout.addWidget(QtWidgets.QLabel(self.tr('thumbnail size: ')), 1, 4) self.size_slider = QtWidgets.QSlider(Qt.Horizontal) self.size_slider.setTracking(False) self.size_slider.setRange(4, 9) self.size_slider.setPageStep(1) self.size_slider.setValue(self.thumb_size // 20) self.size_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) width = self.size_slider.sizeHint().width() self.size_slider.setMinimumWidth(width * 7 // 4) self.size_slider.valueChanged.connect(self._new_thumb_size) layout.addWidget(self.size_slider, 1, 5) def set_drag_to_map(self, icon, hotspot=None): self.drag_icon = icon self.drag_hotspot = hotspot def get_image(self, path): for image in self.images: if image.path == path: return image return None def get_images(self): return self.images @catch_all def mousePressEvent(self, event): if self.scroll_area.underMouse(): self._clear_selection() self.last_selected = None self.selection_anchor = None self.emit_selection() @QtSlot(bool) @catch_all def open_files(self, checked=False): args = [ self, self.tr('Open files'), self.app.config_store.get('paths', 'images', ''), self.tr("Images ({0});;Videos ({1});;All files (*)").format( ' '.join(['*.' + x for x in image_types()]), ' '.join(['*.' + x for x in video_types()])) ] if eval(self.app.config_store.get('pyqt', 'native_dialog', 'True')): pass else: args += [None, QtWidgets.QFileDialog.DontUseNativeDialog] path_list = QtWidgets.QFileDialog.getOpenFileNames(*args) path_list = path_list[0] if not path_list: return self.open_file_list(path_list) @QtSlot(list) @catch_all def open_file_list(self, path_list): with Busy(): for path in path_list: self.open_file(path) self.done_opening(path_list[-1]) def open_file(self, path): path = os.path.abspath(path) if not os.path.isfile(path): return if self.get_image(path): # already opened this path return image = Image(path, self, thumb_size=self.thumb_size) self.images.append(image) self.show_thumbnail(image) def done_opening(self, path): self.app.config_store.set('paths', 'images', os.path.dirname(os.path.abspath(path))) self._sort_thumbnails() def _date_key(self, image): result = image.metadata.date_taken if result is None: result = image.metadata.date_digitised if result is None: result = image.metadata.date_modified if result is None: # use file date as last resort result = datetime.fromtimestamp(os.path.getmtime(image.path)) else: result = result['datetime'] # convert result to string and append path so photos with same # time stamp get sorted consistently result = result.strftime('%Y%m%d%H%M%S%f') + image.path return result @QtSlot() @catch_all def _new_sort_order(self): self._sort_thumbnails() self.sort_order_changed.emit() @QtSlot() @catch_all def _ensure_selected_visible(self): if self.last_selected: self.scroll_area.ensureWidgetVisible(self.last_selected) def _sort_thumbnails(self): sort_date = self.sort_date.isChecked() self.app.config_store.set('controls', 'sort_date', str(sort_date)) with Busy(): if sort_date: self.images.sort(key=self._date_key) else: self.images.sort(key=lambda x: x.path) for image in self.images: self.show_thumbnail(image, False) if self.last_selected: self.app.processEvents() self.scroll_area.ensureWidgetVisible(self.last_selected) self.image_list_changed.emit() def show_thumbnail(self, image, live=True): self.scroll_area.add_widget(image) if live: self.app.processEvents() image.load_thumbnail() if live: self.app.processEvents() self.scroll_area.ensureWidgetVisible(image) self.app.processEvents() def add_selected_actions(self, menu): actions = {} actions['reload'] = menu.addAction('', self.reload_selected_metadata) actions['save'] = menu.addAction( translate('ImageList', 'Save changes'), self.save_selected_metadata) actions['diff'] = menu.addAction( translate('ImageList', 'View changes'), self.diff_selected_metadata) actions['thumbs'] = menu.addAction('', self.regenerate_selected_thumbnails) actions['close'] = menu.addAction('', self.close_selected_files) self.configure_selected_actions(actions) return actions def configure_selected_actions(self, actions): images = self.get_selected_images() changed_images = any([x.metadata.changed() for x in images]) actions['reload'].setEnabled(bool(images)) actions['save'].setEnabled(changed_images) actions['diff'].setEnabled(changed_images) actions['thumbs'].setEnabled(bool(images)) actions['close'].setEnabled(bool(images)) actions['reload'].setText( translate('ImageList', 'Reload file(s)', '', len(images))) actions['thumbs'].setText( translate('ImageList', 'Regenerate thumbnail(s)', '', len(images))) actions['close'].setText( translate('ImageList', 'Close file(s)', '', len(images))) @QtSlot() @catch_all def reload_selected_metadata(self): with Busy(): for image in self.get_selected_images(): image.reload_metadata() @QtSlot() @catch_all def save_selected_metadata(self): self._save_files(images=self.get_selected_images()) @QtSlot() @catch_all def diff_selected_metadata(self): dialog = QtWidgets.QDialog(parent=self) dialog.setLayout(QtWidgets.QVBoxLayout()) dialog.setFixedSize(min(800, self.window().width()), min(400, self.window().height())) table = QtWidgets.QTableWidget() table.setColumnCount(3) table.setHorizontalHeaderLabels([ translate('ImageList', 'new value'), translate('ImageList', 'undo'), translate('ImageList', 'old value') ]) table.horizontalHeader().setSectionResizeMode( 0, QtWidgets.QHeaderView.Stretch) table.horizontalHeader().setSectionResizeMode( 2, QtWidgets.QHeaderView.Stretch) dialog.layout().addWidget(table) button_box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) button_box.accepted.connect(dialog.accept) button_box.rejected.connect(dialog.reject) dialog.layout().addWidget(button_box) changed = False position = None for image in self.get_selected_images(): if not image.metadata.changed(): continue dialog.setWindowTitle( translate('ImageList', 'Metadata differences: {}').format(image.name)) labels = [] row = 0 undo = {} table.clearContents() new_md = image.metadata old_md = Metadata(image.path) for key in ('title', 'description', 'keywords', 'rating', 'copyright', 'creator', 'date_taken', 'date_digitised', 'date_modified', 'orientation', 'camera_model', 'lens_model', 'lens_spec', 'focal_length', 'focal_length_35', 'aperture', 'latlong', 'altitude', 'location_taken', 'location_shown', 'thumbnail'): values = getattr(new_md, key), getattr(old_md, key) if values[0] == values[1]: continue values = [str(x or '') for x in values] table.setRowCount(row + 1) for n, value in enumerate(values): item = QtWidgets.QTableWidgetItem(value) table.setItem(row, n * 2, item) undo[key] = QtWidgets.QTableWidgetItem() undo[key].setFlags(undo[key].flags() | Qt.ItemIsUserCheckable) undo[key].setCheckState(Qt.Unchecked) table.setItem(row, 1, undo[key]) labels.append(key) row += 1 if not row: continue table.setVerticalHeaderLabels(labels) table.resizeColumnsToContents() table.resizeRowsToContents() if position: dialog.move(position) if dialog.exec_() != QtWidgets.QDialog.Accepted: return position = dialog.pos() undo_all = True for key, widget in undo.items(): if widget.checkState() == Qt.Checked: setattr(new_md, key, getattr(old_md, key)) changed = True else: undo_all = False if undo_all: image.reload_metadata() if changed: self.emit_selection() @QtSlot() @catch_all def regenerate_selected_thumbnails(self): with Busy(): for image in self.get_selected_images(): if image.regenerate_thumbnail(): image.load_thumbnail() @QtSlot() @catch_all def close_selected_files(self): self.close_files(False) @QtSlot() @catch_all def close_all_files(self): self.close_files(True) def close_files(self, all_files): if not self.unsaved_files_dialog(all_files=all_files): return if all_files: close_list = list(self.images) else: close_list = self.get_selected_images() if not close_list: return idx = self.images.index(close_list[0]) for image in close_list: self.images.remove(image) self.scroll_area.remove_widget(image) image.setParent(None) if 0 <= idx < len(self.images): self.select_image(self.images[idx]) else: self.last_selected = None self.selection_anchor = None self.emit_selection() self.image_list_changed.emit() @QtSlot(bool) @catch_all def save_files(self, checked=False): self._save_files(self.images) def _save_files(self, images=[]): if_mode = eval(self.app.config_store.get('files', 'image', 'True')) sc_mode = self.app.config_store.get('files', 'sidecar', 'auto') force_iptc = eval( self.app.config_store.get('files', 'force_iptc', 'False')) keep_time = eval( self.app.config_store.get('files', 'preserve_timestamps', 'False')) if not images: images = self.images with Busy(): for image in images: if keep_time: file_times = image.file_times else: file_times = None image.metadata.save(if_mode=if_mode, sc_mode=sc_mode, force_iptc=force_iptc, file_times=file_times) unsaved = False for image in self.images: if image.metadata.changed(): unsaved = True break self.new_metadata.emit(unsaved) def unsaved_files_dialog(self, all_files=False, with_cancel=True, with_discard=True): """Return true if OK to continue with close or quit or whatever""" for image in self.images: if image.metadata.changed() and (all_files or image.selected): break else: return True dialog = QtWidgets.QMessageBox(parent=self) dialog.setWindowTitle(self.tr('Photini: unsaved data')) dialog.setText(self.tr('<h3>Some images have unsaved metadata.</h3>')) dialog.setInformativeText(self.tr('Do you want to save your changes?')) dialog.setIcon(QtWidgets.QMessageBox.Warning) buttons = QtWidgets.QMessageBox.Save if with_cancel: buttons |= QtWidgets.QMessageBox.Cancel if with_discard: buttons |= QtWidgets.QMessageBox.Discard dialog.setStandardButtons(buttons) dialog.setDefaultButton(QtWidgets.QMessageBox.Save) result = dialog.exec_() if result == QtWidgets.QMessageBox.Save: self._save_files() return True return result == QtWidgets.QMessageBox.Discard def get_selected_images(self): selection = [] for image in self.images: if image.get_selected(): selection.append(image) return selection def emit_selection(self): self.selection_changed.emit(self.get_selected_images()) def select_all(self): for image in self.images: image.set_selected(True) self.selection_anchor = None self.last_selected = None self.emit_selection() def move_to_prev_thumb(self): self._inc_selection(-1) def move_to_next_thumb(self): self._inc_selection(1) def move_to_first_thumb(self): self.select_image(self.images[0]) def move_to_last_thumb(self): self.select_image(self.images[-1]) def select_prev_thumb(self): self._inc_selection(-1, extend_selection=True) def select_next_thumb(self): self._inc_selection(1, extend_selection=True) def _inc_selection(self, inc, extend_selection=False): if self.last_selected: idx = self.images.index(self.last_selected) idx = (idx + inc) % len(self.images) else: idx = 0 self.select_image(self.images[idx], extend_selection=extend_selection) @QtSlot(int) @catch_all def _new_thumb_size(self, value): self.thumb_size = value * 20 self.app.config_store.set('controls', 'thumb_size', str(self.thumb_size)) for image in self.images: image.set_thumb_size(self.thumb_size) if self.last_selected: self.app.processEvents() self.scroll_area.ensureWidgetVisible(self.last_selected) def select_image(self, image, extend_selection=False, multiple_selection=False): self.scroll_area.ensureWidgetVisible(image) if extend_selection and self.selection_anchor: idx1 = self.images.index(self.selection_anchor) idx2 = self.images.index(self.last_selected) for i in range(min(idx1, idx2), max(idx1, idx2) + 1): self.images[i].set_selected(False) idx2 = self.images.index(image) for i in range(min(idx1, idx2), max(idx1, idx2) + 1): self.images[i].set_selected(True) elif multiple_selection: image.set_selected(not image.get_selected()) self.selection_anchor = image else: self._clear_selection() image.set_selected(True) self.selection_anchor = image self.last_selected = image self.emit_selection() def select_images(self, images): self._clear_selection() if not images: self.last_selected = None self.selection_anchor = None self.emit_selection() return for image in images: image.set_selected(True) self.scroll_area.ensureWidgetVisible(image) self.selection_anchor = images[0] self.last_selected = images[-1] self.emit_selection() def _clear_selection(self): for image in self.images: if image.get_selected(): image.set_selected(False)