def __init__(self, device, rules): QGroupBox.__init__(self, _('Format specific sending')) self._device = weakref.ref(device) self.l = l = QVBoxLayout() self.setLayout(l) self.la = la = QLabel('<p>'+_( '''You can create rules that control where e-books of a specific format are sent to on the device. These will take precedence over the folders specified above.''')) la.setWordWrap(True) l.addWidget(la) self.sa = sa = QScrollArea(self) sa.setWidgetResizable(True) self.w = w = QWidget(self) w.l = QVBoxLayout() w.setLayout(w.l) sa.setWidget(w) l.addWidget(sa) self.widgets = [] for rule in rules: r = Rule(device, rule) self.widgets.append(r) w.l.addWidget(r) r.remove.connect(self.remove_rule) if not self.widgets: self.add_rule() self.b = b = QPushButton(QIcon(I('plus.png')), _('Add a &new rule')) l.addWidget(b) b.clicked.connect(self.add_rule) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Ignored)
def setup_ui(self): acnames = all_actions().all_action_names self.available_actions = ActionsList(acnames - frozenset(current_actions()), parent=self) self.available_actions.itemDoubleClicked.connect(self.add_item) self.current_actions = ActionsList(current_actions(), parent=self, is_source=False) self.current_actions.itemDoubleClicked.connect(self.remove_item) self.l = l = QVBoxLayout(self) self.la = la = QLabel(_('Choose the actions you want on the toolbar.' ' Drag and drop items in the right hand list to re-arrange the toolbar.')) la.setWordWrap(True) l.addWidget(la) self.bv = bv = QVBoxLayout() bv.addStretch(10) self.add_button = b = QToolButton(self) b.setIcon(QIcon(I('forward.png'))), b.setToolTip(_('Add selected actions to the toolbar')) bv.addWidget(b), bv.addStretch(10) b.clicked.connect(self.add_actions) self.remove_button = b = QToolButton(self) b.setIcon(QIcon(I('back.png'))), b.setToolTip(_('Remove selected actions from the toolbar')) b.clicked.connect(self.remove_actions) bv.addWidget(b), bv.addStretch(10) self.h = h = QHBoxLayout() l.addLayout(h) self.lg = lg = QGroupBox(_('A&vailable actions'), self) lg.v = v = QVBoxLayout(lg) v.addWidget(self.available_actions) h.addWidget(lg) self.rg = rg = QGroupBox(_('&Current actions'), self) rg.v = v = QVBoxLayout(rg) v.addWidget(self.current_actions) h.addLayout(bv), h.addWidget(rg) l.addWidget(self.bb) self.rdb = b = self.bb.addButton(_('Restore defaults'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.restore_defaults)
def __init__(self, plugin): QWidget.__init__(self) self.plugin = plugin self.overl = l = QVBoxLayout(self) self.gb = QGroupBox(_('Metadata fields to download'), self) if plugin.config_help_message: self.pchm = QLabel(plugin.config_help_message) self.pchm.setWordWrap(True) self.pchm.setOpenExternalLinks(True) l.addWidget(self.pchm, 10) l.addWidget(self.gb) self.gb.l = g = QVBoxLayout(self.gb) g.setContentsMargins(0, 0, 0, 0) self.fields_view = v = FieldsList(self) g.addWidget(v) v.setFlow(QListView.Flow.LeftToRight) v.setWrapping(True) v.setResizeMode(QListView.ResizeMode.Adjust) self.fields_model = FieldsModel(self.plugin) self.fields_model.initialize() v.setModel(self.fields_model) self.memory = [] self.widgets = [] self.l = QGridLayout() self.l.setContentsMargins(0, 0, 0, 0) l.addLayout(self.l, 100) for opt in plugin.options: self.create_widgets(opt)
def setup_ui(self): self.l = l = QVBoxLayout(self) self.setLayout(l) self.gb = gb = QGroupBox(_('&Images in book'), self) self.v = v = QVBoxLayout(gb) gb.setLayout(v), gb.setFlat(True) self.names, self.names_filter = create_filterable_names_list( sorted(self.image_names, key=sort_key), filter_text=_('Filter the list of images'), parent=self) self.names.doubleClicked.connect( self.double_clicked, type=Qt.ConnectionType.QueuedConnection) self.cover_view = CoverView(self) l.addWidget(self.names_filter) v.addWidget(self.names) self.splitter = s = QSplitter(self) l.addWidget(s) s.addWidget(gb) s.addWidget(self.cover_view) self.h = h = QHBoxLayout() self.preserve = p = QCheckBox(_('Preserve aspect ratio')) p.setToolTip( textwrap.fill( _('If enabled the cover image you select will be embedded' ' into the book in such a way that when viewed, its aspect' ' ratio (ratio of width to height) will be preserved.' ' This will mean blank spaces around the image if the screen' ' the book is being viewed on has an aspect ratio different' ' to the image.'))) p.setChecked(tprefs['add_cover_preserve_aspect_ratio']) p.setVisible(self.container.book_type != 'azw3') def on_state_change(s): tprefs.set('add_cover_preserve_aspect_ratio', s == Qt.CheckState.Checked) p.stateChanged.connect(on_state_change) self.info_label = il = QLabel('\xa0') h.addWidget(p), h.addStretch(1), h.addWidget(il) l.addLayout(h) l.addWidget(self.bb) b = self.bb.addButton(_('Import &image'), QDialogButtonBox.ButtonRole.ActionRole) b.clicked.connect(self.import_image) b.setIcon(QIcon(I('document_open.png'))) self.names.setFocus(Qt.FocusReason.OtherFocusReason) self.names.selectionModel().currentChanged.connect( self.current_image_changed) cname = get_raster_cover_name(self.container) if cname: row = self.names.model().find_name(cname) if row > -1: self.names.setCurrentIndex(self.names.model().index(row))
def setup_ui(self): self.splitter = QSplitter(self) self.l = l = QVBoxLayout(self) l.addWidget(self.splitter) l.addWidget(self.bb) self.w = w = QGroupBox(_('Theme Metadata'), self) self.splitter.addWidget(w) l = w.l = QFormLayout(w) l.setFieldGrowthPolicy( QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self.missing_icons_group = mg = QGroupBox(self) self.mising_icons = mi = QListWidget(mg) mi.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) mg.l = QVBoxLayout(mg) mg.l.addWidget(mi) self.splitter.addWidget(mg) self.title = QLineEdit(self) l.addRow(_('&Title:'), self.title) self.author = QLineEdit(self) l.addRow(_('&Author:'), self.author) self.version = v = QSpinBox(self) v.setMinimum(1), v.setMaximum(1000000) l.addRow(_('&Version:'), v) self.license = lc = QLineEdit(self) l.addRow(_('&License:'), lc) self.url = QLineEdit(self) l.addRow(_('&URL:'), self.url) lc.setText( _('The license for the icons in this theme. Common choices are' ' Creative Commons or Public Domain.')) self.description = QTextEdit(self) l.addRow(self.description) self.refresh_button = rb = self.bb.addButton( _('&Refresh'), QDialogButtonBox.ButtonRole.ActionRole) rb.setIcon(QIcon(I('view-refresh.png'))) rb.clicked.connect(self.refresh) self.apply_report()
def setup_ui(self): # {{{ self._g = g = QHBoxLayout(self) self.setLayout(g) self._l = l = QVBoxLayout() g.addLayout(l) fmts = sorted(x.upper() for x in self.fmts) self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'), self) self._fl = fl = QHBoxLayout() self.fmt_choice_box.setLayout(self._fl) self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts] for x in self.fmt_choice_buttons: fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else 0) l.addWidget(self.fmt_choice_box) self.fmt_choice_box.setVisible(len(fmts) > 1) self.help_label = QLabel(_('''\ <h2>About Unpack book</h2> <p>Unpack book allows you to fine tune the appearance of an e-book by making small changes to its internals. In order to use Unpack book, you need to know a little bit about HTML and CSS, technologies that are used in e-books. Follow the steps:</p> <br> <ol> <li>Click "Explode book": This will "explode" the book into its individual internal components.<br></li> <li>Right click on any individual file and select "Open with..." to edit it in your favorite text editor.<br></li> <li>When you are done: <b>close the file browser window and the editor windows you used to make your tweaks</b>. Then click the "Rebuild book" button, to update the book in your calibre library.</li> </ol>''')) self.help_label.setWordWrap(True) self._fr = QFrame() self._fr.setFrameShape(QFrame.Shape.VLine) g.addWidget(self._fr) g.addWidget(self.help_label) self._b = b = QGridLayout() left, top, right, bottom = b.getContentsMargins() top += top b.setContentsMargins(left, top, right, bottom) l.addLayout(b, stretch=10) self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode book')) self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview book')) self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel')) self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild book')) self.explode_button.setToolTip( _('Explode the book to edit its components')) self.preview_button.setToolTip( _('Preview the result of your changes')) self.cancel_button.setToolTip( _('Abort without saving any changes')) self.rebuild_button.setToolTip( _('Save your changes and update the book in the calibre library')) a = b.addWidget a(self.explode_button, 0, 0, 1, 1) a(self.preview_button, 0, 1, 1, 1) a(self.cancel_button, 1, 0, 1, 1) a(self.rebuild_button, 1, 1, 1, 1) for x in ('explode', 'preview', 'cancel', 'rebuild'): getattr(self, x+'_button').clicked.connect(getattr(self, x)) self.msg = QLabel('dummy', self) self.msg.setVisible(False) self.msg.setStyleSheet(''' QLabel { text-align: center; background-color: white; color: black; border-width: 1px; border-style: solid; border-radius: 20px; font-size: x-large; font-weight: bold; } ''') self.resize(self.sizeHint() + QSize(40, 10))
class UnpackBook(QDialog): def __init__(self, parent, book_id, fmts, db): QDialog.__init__(self, parent) self.setWindowIcon(QIcon(I('unpack-book.png'))) self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db) self._exploded = None self._cleanup_dirs = [] self._cleanup_files = [] self.setup_ui() self.setWindowTitle(_('Unpack book') + ' - ' + db.title(book_id, index_is_id=True)) button = self.fmt_choice_buttons[0] button_map = {str(x.text()):x for x in self.fmt_choice_buttons} of = prefs['output_format'].upper() df = tweaks.get('default_tweak_format', None) lf = gprefs.get('last_tweak_format', None) if df and df.lower() == 'remember' and lf in button_map: button = button_map[lf] elif df and df.upper() in button_map: button = button_map[df.upper()] elif of in button_map: button = button_map[of] button.setChecked(True) self.init_state() for button in self.fmt_choice_buttons: button.toggled.connect(self.init_state) def init_state(self, *args): self._exploded = None self.preview_button.setEnabled(False) self.rebuild_button.setEnabled(False) self.explode_button.setEnabled(True) def setup_ui(self): # {{{ self._g = g = QHBoxLayout(self) self.setLayout(g) self._l = l = QVBoxLayout() g.addLayout(l) fmts = sorted(x.upper() for x in self.fmts) self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'), self) self._fl = fl = QHBoxLayout() self.fmt_choice_box.setLayout(self._fl) self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts] for x in self.fmt_choice_buttons: fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else 0) l.addWidget(self.fmt_choice_box) self.fmt_choice_box.setVisible(len(fmts) > 1) self.help_label = QLabel(_('''\ <h2>About Unpack book</h2> <p>Unpack book allows you to fine tune the appearance of an e-book by making small changes to its internals. In order to use Unpack book, you need to know a little bit about HTML and CSS, technologies that are used in e-books. Follow the steps:</p> <br> <ol> <li>Click "Explode book": This will "explode" the book into its individual internal components.<br></li> <li>Right click on any individual file and select "Open with..." to edit it in your favorite text editor.<br></li> <li>When you are done: <b>close the file browser window and the editor windows you used to make your tweaks</b>. Then click the "Rebuild book" button, to update the book in your calibre library.</li> </ol>''')) self.help_label.setWordWrap(True) self._fr = QFrame() self._fr.setFrameShape(QFrame.Shape.VLine) g.addWidget(self._fr) g.addWidget(self.help_label) self._b = b = QGridLayout() left, top, right, bottom = b.getContentsMargins() top += top b.setContentsMargins(left, top, right, bottom) l.addLayout(b, stretch=10) self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode book')) self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview book')) self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel')) self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild book')) self.explode_button.setToolTip( _('Explode the book to edit its components')) self.preview_button.setToolTip( _('Preview the result of your changes')) self.cancel_button.setToolTip( _('Abort without saving any changes')) self.rebuild_button.setToolTip( _('Save your changes and update the book in the calibre library')) a = b.addWidget a(self.explode_button, 0, 0, 1, 1) a(self.preview_button, 0, 1, 1, 1) a(self.cancel_button, 1, 0, 1, 1) a(self.rebuild_button, 1, 1, 1, 1) for x in ('explode', 'preview', 'cancel', 'rebuild'): getattr(self, x+'_button').clicked.connect(getattr(self, x)) self.msg = QLabel('dummy', self) self.msg.setVisible(False) self.msg.setStyleSheet(''' QLabel { text-align: center; background-color: white; color: black; border-width: 1px; border-style: solid; border-radius: 20px; font-size: x-large; font-weight: bold; } ''') self.resize(self.sizeHint() + QSize(40, 10)) # }}} def show_msg(self, msg): self.msg.setText(msg) self.msg.resize(self.size() - QSize(50, 25)) self.msg.move((self.width() - self.msg.width())//2, (self.height() - self.msg.height())//2) self.msg.setVisible(True) def hide_msg(self): self.msg.setVisible(False) def explode(self): self.show_msg(_('Exploding, please wait...')) if len(self.fmt_choice_buttons) > 1: gprefs.set('last_tweak_format', self.current_format.upper()) QTimer.singleShot(5, self.do_explode) def ask_question(self, msg): return question_dialog(self, _('Are you sure?'), msg) def do_explode(self): from calibre.ebooks.tweak import get_tools, Error, WorkerError tdir = PersistentTemporaryDirectory('_tweak_explode') self._cleanup_dirs.append(tdir) det_msg = None try: src = self.db.format(self.book_id, self.current_format, index_is_id=True, as_path=True) self._cleanup_files.append(src) exploder = get_tools(self.current_format)[0] opf = exploder(src, tdir, question=self.ask_question) except WorkerError as e: det_msg = e.orig_tb except Error as e: return error_dialog(self, _('Failed to unpack'), (_('Could not explode the %s file.')%self.current_format) + ' ' + as_unicode(e), show=True) except: import traceback det_msg = traceback.format_exc() finally: self.hide_msg() if det_msg is not None: return error_dialog(self, _('Failed to unpack'), _('Could not explode the %s file. Click "Show details" for ' 'more information.')%self.current_format, det_msg=det_msg, show=True) if opf is None: # The question was answered with No return self._exploded = tdir self.explode_button.setEnabled(False) self.preview_button.setEnabled(True) self.rebuild_button.setEnabled(True) open_local_file(tdir) def rebuild_it(self): from calibre.ebooks.tweak import get_tools, WorkerError src_dir = self._exploded det_msg = None of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower()) of.close() of = of.name self._cleanup_files.append(of) try: rebuilder = get_tools(self.current_format)[1] rebuilder(src_dir, of) except WorkerError as e: det_msg = e.orig_tb except: import traceback det_msg = traceback.format_exc() finally: self.hide_msg() if det_msg is not None: error_dialog(self, _('Failed to rebuild file'), _('Failed to rebuild %s. For more information, click ' '"Show details".')%self.current_format, det_msg=det_msg, show=True) return None return of def preview(self): self.show_msg(_('Rebuilding, please wait...')) QTimer.singleShot(5, self.do_preview) def do_preview(self): rebuilt = self.rebuild_it() if rebuilt is not None: self.parent().iactions['View']._view_file(rebuilt) def rebuild(self): self.show_msg(_('Rebuilding, please wait...')) QTimer.singleShot(5, self.do_rebuild) def do_rebuild(self): rebuilt = self.rebuild_it() if rebuilt is not None: fmt = os.path.splitext(rebuilt)[1][1:].upper() with open(rebuilt, 'rb') as f: self.db.add_format(self.book_id, fmt, f, index_is_id=True) self.accept() def cancel(self): self.reject() def cleanup(self): if ismacos and self._exploded: try: import appscript self.finder = appscript.app('Finder') self.finder.Finder_windows[os.path.basename(self._exploded)].close() except: pass for f in self._cleanup_files: try: os.remove(f) except: pass for d in self._cleanup_dirs: try: shutil.rmtree(d) except: pass @property def db(self): return self.db_ref() @property def current_format(self): for b in self.fmt_choice_buttons: if b.isChecked(): return str(b.text())
def __init__(self, recipe_model, parent=None): QDialog.__init__(self, parent) self.commit_on_change = True self.previous_urn = None self.setWindowIcon(QIcon(I('scheduler.png'))) self.l = l = QGridLayout(self) # Left panel self.h = h = QHBoxLayout() l.addLayout(h, 0, 0, 1, 1) self.search = s = SearchBox2(self) self.search.initialize('scheduler_search_history') self.search.setMinimumContentsLength(15) self.go_button = b = QToolButton(self) b.setText(_("Go")) b.clicked.connect(self.search.do_search) h.addWidget(s), h.addWidget(b) self.recipes = RecipesView(self) l.addWidget(self.recipes, 1, 0, 2, 1) self.recipe_model = recipe_model self.recipe_model.do_refresh() self.recipes.setModel(self.recipe_model) self.recipes.setFocus(Qt.FocusReason.OtherFocusReason) self.recipes.item_activated.connect(self.download_clicked) self.setWindowTitle( _("Schedule news download [{} sources]").format( self.recipe_model.showing_count)) self.search.search.connect(self.recipe_model.search) self.recipe_model.searched.connect( self.search.search_done, type=Qt.ConnectionType.QueuedConnection) self.recipe_model.searched.connect(self.search_done) # Right Panel self.scroll_area_contents = sac = QWidget(self) self.l.addWidget(sac, 0, 1, 2, 1) sac.v = v = QVBoxLayout(sac) v.setContentsMargins(0, 0, 0, 0) self.detail_box = QTabWidget(self) self.detail_box.setVisible(False) self.detail_box.setCurrentIndex(0) v.addWidget(self.detail_box) v.addItem( QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)) # First Tab (scheduling) self.tab = QWidget() self.detail_box.addTab(self.tab, _("&Schedule")) self.tab.v = vt = QVBoxLayout(self.tab) vt.setContentsMargins(0, 0, 0, 0) self.blurb = la = QLabel('blurb') la.setWordWrap(True), la.setOpenExternalLinks(True) vt.addWidget(la) self.frame = f = QFrame(self.tab) vt.addWidget(f) f.setFrameShape(QFrame.Shape.StyledPanel) f.setFrameShadow(QFrame.Shadow.Raised) f.v = vf = QVBoxLayout(f) self.schedule = s = QCheckBox(_("&Schedule for download:"), f) self.schedule.stateChanged[int].connect(self.toggle_schedule_info) vf.addWidget(s) f.h = h = QHBoxLayout() vf.addLayout(h) self.days_of_week = QRadioButton(_("&Days of week"), f) self.days_of_month = QRadioButton(_("Da&ys of month"), f) self.every_x_days = QRadioButton(_("Every &x days"), f) self.days_of_week.setChecked(True) h.addWidget(self.days_of_week), h.addWidget( self.days_of_month), h.addWidget(self.every_x_days) self.schedule_stack = ss = QStackedWidget(f) self.schedule_widgets = [] for key in reversed(self.SCHEDULE_TYPES): self.schedule_widgets.insert(0, self.SCHEDULE_TYPES[key](self)) self.schedule_stack.insertWidget(0, self.schedule_widgets[0]) vf.addWidget(ss) self.last_downloaded = la = QLabel(f) la.setWordWrap(True) vf.addWidget(la) self.account = acc = QGroupBox(self.tab) acc.setTitle(_("&Account")) vt.addWidget(acc) acc.g = g = QGridLayout(acc) acc.unla = la = QLabel(_("&Username:"******"&Password:"******"&Show password"), self.account) spw.stateChanged[int].connect(self.set_pw_echo_mode) g.addWidget(spw, 2, 0, 1, 2) self.rla = la = QLabel( _("For the scheduling to work, you must leave calibre running.")) vt.addWidget(la) for b, c in iteritems(self.SCHEDULE_TYPES): b = getattr(self, b) b.toggled.connect(self.schedule_type_selected) b.setToolTip(textwrap.dedent(c.HELP)) # Second tab (advanced settings) self.tab2 = t2 = QWidget() self.detail_box.addTab(self.tab2, _("&Advanced")) self.tab2.g = g = QGridLayout(t2) g.setContentsMargins(0, 0, 0, 0) self.add_title_tag = tt = QCheckBox(_("Add &title as tag"), t2) g.addWidget(tt, 0, 0, 1, 2) t2.la = la = QLabel(_("&Extra tags:")) self.custom_tags = ct = QLineEdit(self) la.setBuddy(ct) g.addWidget(la), g.addWidget(ct, 1, 1) t2.la2 = la = QLabel(_("&Keep at most:")) la.setToolTip( _("Maximum number of copies (issues) of this recipe to keep. Set to 0 to keep all (disable)." )) self.keep_issues = ki = QSpinBox(t2) tt.toggled['bool'].connect(self.keep_issues.setEnabled) ki.setMaximum(100000), la.setBuddy(ki) ki.setToolTip( _("<p>When set, this option will cause calibre to keep, at most, the specified number of issues" " of this periodical. Every time a new issue is downloaded, the oldest one is deleted, if the" " total is larger than this number.\n<p>Note that this feature only works if you have the" " option to add the title as tag checked, above.\n<p>Also, the setting for deleting periodicals" " older than a number of days, below, takes priority over this setting." )) ki.setSpecialValueText(_("all issues")), ki.setSuffix(_(" issues")) g.addWidget(la), g.addWidget(ki, 2, 1) si = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) g.addItem(si, 3, 1, 1, 1) # Bottom area self.hb = h = QHBoxLayout() self.l.addLayout(h, 2, 1, 1, 1) self.labt = la = QLabel(_("Delete downloaded &news older than:")) self.old_news = on = QSpinBox(self) on.setToolTip( _("<p>Delete downloaded news older than the specified number of days. Set to zero to disable.\n" "<p>You can also control the maximum number of issues of a specific periodical that are kept" " by clicking the Advanced tab for that periodical above.")) on.setSpecialValueText(_("never delete")), on.setSuffix(_(" days")) on.setMaximum(1000), la.setBuddy(on) on.setValue(gconf['oldest_news']) h.addWidget(la), h.addWidget(on) self.download_all_button = b = QPushButton( QIcon(I('news.png')), _("Download &all scheduled"), self) b.setToolTip(_("Download all scheduled news sources at once")) b.clicked.connect(self.download_all_clicked) self.l.addWidget(b, 3, 0, 1, 1) self.bb = bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self) bb.accepted.connect(self.accept), bb.rejected.connect(self.reject) self.download_button = b = bb.addButton( _('&Download now'), QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon(I('arrow-down.png'))), b.setVisible(False) b.clicked.connect(self.download_clicked) self.l.addWidget(bb, 3, 1, 1, 1) geom = gprefs.get('scheduler_dialog_geometry') if geom is not None: QApplication.instance().safe_restore_geometry(self, geom)
def setupUi(self, x): self.l = l = QVBoxLayout(self) self.la1 = la = QLabel( _("Values for the tweaks are shown below. Edit them to change the behavior of calibre." " Your changes will only take effect <b>after a restart</b> of calibre." )) l.addWidget(la), la.setWordWrap(True) self.splitter = s = QSplitter(self) s.setChildrenCollapsible(False) l.addWidget(s, 10) self.lv = lv = QWidget(self) lv.l = l2 = QVBoxLayout(lv) l2.setContentsMargins(0, 0, 0, 0) self.tweaks_view = tv = TweaksView(self) l2.addWidget(tv) self.plugin_tweaks_button = b = QPushButton(self) b.setToolTip( _("Edit tweaks for any custom plugins you have installed")) b.setText(_("&Plugin tweaks")) l2.addWidget(b) s.addWidget(lv) self.lv1 = lv = QWidget(self) s.addWidget(lv) lv.g = g = QGridLayout(lv) g.setContentsMargins(0, 0, 0, 0) self.search = sb = SearchBox2(self) sb.sizePolicy().setHorizontalStretch(10) sb.setSizeAdjustPolicy( QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLength) sb.setMinimumContentsLength(10) g.setColumnStretch(0, 100) g.addWidget(self.search, 0, 0, 1, 1) self.next_button = b = QPushButton(self) b.setIcon(QIcon(I("arrow-down.png"))) b.setText(_("&Next")) g.addWidget(self.next_button, 0, 1, 1, 1) self.previous_button = b = QPushButton(self) b.setIcon(QIcon(I("arrow-up.png"))) b.setText(_("&Previous")) g.addWidget(self.previous_button, 0, 2, 1, 1) self.hb = hb = QGroupBox(self) hb.setTitle(_("Help")) hb.l = l2 = QVBoxLayout(hb) self.help = h = QPlainTextEdit(self) l2.addWidget(h) h.setReadOnly(True) g.addWidget(hb, 1, 0, 1, 3) self.eb = eb = QGroupBox(self) g.addWidget(eb, 2, 0, 1, 3) eb.setTitle(_("Edit tweak")) eb.g = ebg = QGridLayout(eb) self.edit_tweak = et = QPlainTextEdit(self) et.setMinimumWidth(400) et.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) ebg.addWidget(et, 0, 0, 1, 2) self.restore_default_button = b = QPushButton(self) b.setToolTip(_("Restore this tweak to its default value")) b.setText(_("&Reset this tweak")) ebg.addWidget(b, 1, 0, 1, 1) self.apply_button = ab = QPushButton(self) ab.setToolTip(_("Apply any changes you made to this tweak")) ab.setText(_("&Apply changes to this tweak")) ebg.addWidget(ab, 1, 1, 1, 1)
def __init__(self, parent, device=None, title=_("Unknown")): QGroupBox.__init__(self, parent) self.device = device self.setTitle(title)
def __init__(self, extra_customization_message, extra_customization_choices, device_settings): super(ExtraCustomization, self).__init__() debug_print( "ExtraCustomization.__init__ - extra_customization_message=", extra_customization_message) debug_print( "ExtraCustomization.__init__ - extra_customization_choices=", extra_customization_choices) debug_print( "ExtraCustomization.__init__ - device_settings.extra_customization=", device_settings.extra_customization) debug_print("ExtraCustomization.__init__ - device_settings=", device_settings) self.extra_customization_message = extra_customization_message self.l = QVBoxLayout(self) self.setLayout(self.l) options_group = QGroupBox(_("Extra driver customization options"), self) self.l.addWidget(options_group) self.extra_layout = QGridLayout() self.extra_layout.setObjectName("extra_layout") options_group.setLayout(self.extra_layout) if extra_customization_message: extra_customization_choices = extra_customization_choices or {} def parse_msg(m): msg, _, tt = m.partition(':::') if m else ('', '', '') return msg.strip(), textwrap.fill(tt.strip(), 100) if isinstance(extra_customization_message, list): self.opt_extra_customization = [] if len(extra_customization_message) > 6: row_func = lambda x, y: ((x // 2) * 2) + y col_func = lambda x: x % 2 else: row_func = lambda x, y: x * 2 + y col_func = lambda x: 0 for i, m in enumerate(extra_customization_message): label_text, tt = parse_msg(m) if not label_text: self.opt_extra_customization.append(None) continue if isinstance(device_settings.extra_customization[i], bool): self.opt_extra_customization.append( QCheckBox(label_text)) self.opt_extra_customization[-1].setToolTip(tt) self.opt_extra_customization[i].setChecked( bool(device_settings.extra_customization[i])) elif i in extra_customization_choices: cb = QComboBox(self) self.opt_extra_customization.append(cb) l = QLabel(label_text) l.setToolTip(tt), cb.setToolTip(tt), l.setBuddy( cb), cb.setToolTip(tt) for li in sorted(extra_customization_choices[i]): self.opt_extra_customization[i].addItem(li) cb.setCurrentIndex( max( 0, cb.findText( device_settings.extra_customization[i]))) else: self.opt_extra_customization.append(QLineEdit(self)) l = QLabel(label_text) l.setToolTip(tt) self.opt_extra_customization[i].setToolTip(tt) l.setBuddy(self.opt_extra_customization[i]) l.setWordWrap(True) self.opt_extra_customization[i].setText( device_settings.extra_customization[i]) self.opt_extra_customization[i].setCursorPosition(0) self.extra_layout.addWidget(l, row_func(i + 2, 0), col_func(i)) self.extra_layout.addWidget( self.opt_extra_customization[i], row_func(i + 2, 1), col_func(i)) spacerItem1 = QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) self.extra_layout.addItem(spacerItem1, row_func(i + 2 + 2, 1), 0, 1, 2) self.extra_layout.setRowStretch(row_func(i + 2 + 2, 1), 2) else: self.opt_extra_customization = QLineEdit() label_text, tt = parse_msg(extra_customization_message) l = QLabel(label_text) l.setToolTip(tt) l.setBuddy(self.opt_extra_customization) l.setWordWrap(True) if device_settings.extra_customization: self.opt_extra_customization.setText( device_settings.extra_customization) self.opt_extra_customization.setCursorPosition(0) self.opt_extra_customization.setCursorPosition(0) self.extra_layout.addWidget(l, 0, 0) self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
def __init__(self, parent=None): QWidget.__init__(self, parent) self.l = gl = QGridLayout(self) self.changed = False self.bars = b = QComboBox(self) b.addItem(_('Choose which toolbar you want to customize')) ft = _('Tools for %s editors') for name, text in ( ( 'global_book_toolbar', _('Book wide actions'), ), ( 'global_tools_toolbar', _('Book wide tools'), ), ( 'global_plugins_toolbar', _('Book wide tools from third party plugins'), ), ('editor_common_toolbar', _('Common tools for all editors')), ( 'editor_html_toolbar', ft % 'HTML', ), ( 'editor_css_toolbar', ft % 'CSS', ), ( 'editor_xml_toolbar', ft % 'XML', ), ( 'editor_format_toolbar', _('Text formatting actions'), ), ): b.addItem(text, name) self.la = la = QLabel(_('&Toolbar to customize:')) la.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) la.setBuddy(b) gl.addWidget(la), gl.addWidget(b, 0, 1) self.sl = l = QGridLayout() gl.addLayout(l, 1, 0, 1, -1) self.gb1 = gb1 = QGroupBox(_('A&vailable actions'), self) self.gb2 = gb2 = QGroupBox(_('&Current actions'), self) gb1.setFlat(True), gb2.setFlat(True) gb1.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) gb2.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) l.addWidget(gb1, 0, 0, -1, 1), l.addWidget(gb2, 0, 2, -1, 1) self.available, self.current = ToolbarList(self), ToolbarList(self) self.available.itemDoubleClicked.connect(self.add_single_action) self.current.itemDoubleClicked.connect(self.remove_single_action) self.ub = b = QToolButton(self) connect_lambda(b.clicked, self, lambda self: self.move(up=True)) b.setToolTip(_('Move selected action up')), b.setIcon( QIcon(I('arrow-up.png'))) self.db = b = QToolButton(self) connect_lambda(b.clicked, self, lambda self: self.move(up=False)) b.setToolTip(_('Move selected action down')), b.setIcon( QIcon(I('arrow-down.png'))) self.gl1 = gl1 = QVBoxLayout() gl1.addWidget(self.available), gb1.setLayout(gl1) self.gl2 = gl2 = QGridLayout() gl2.addWidget(self.current, 0, 0, -1, 1) gl2.addWidget(self.ub, 0, 1), gl2.addWidget(self.db, 2, 1) gb2.setLayout(gl2) self.lb = b = QToolButton(self) b.setToolTip(_('Add selected actions to the toolbar')), b.setIcon( QIcon(I('forward.png'))) l.addWidget(b, 1, 1), b.clicked.connect(self.add_action) self.rb = b = QToolButton(self) b.setToolTip(_('Remove selected actions from the toolbar')), b.setIcon( QIcon(I('back.png'))) l.addWidget(b, 3, 1), b.clicked.connect(self.remove_action) self.si = QSpacerItem(20, 10, hPolicy=QSizePolicy.Policy.Preferred, vPolicy=QSizePolicy.Policy.Expanding) l.setRowStretch(0, 10), l.setRowStretch(2, 10), l.setRowStretch(4, 10) l.addItem(self.si, 4, 1) self.read_settings() self.toggle_visibility(False) self.bars.currentIndexChanged.connect(self.bar_changed) self.toolbar_icon_size = ics = QSpinBox(self) ics.setMinimum(16), ics.setMaximum(128), ics.setSuffix( ' px'), ics.setValue(tprefs['toolbar_icon_size']) ics.setToolTip('<p>' + _('Adjust the size of icons on all toolbars')) self.h = h = QHBoxLayout() gl.addLayout(h, gl.rowCount(), 0, 1, -1) self.toolbar_icon_size_label = la = QLabel(_('Toolbar &icon size:')) la.setBuddy(ics) h.addWidget(la), h.addWidget(ics), h.addStretch(10)
def __init__(self, parent): QWidget.__init__(self, parent) self.l = l = QFormLayout(self) l.setFieldGrowthPolicy( QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self.hm = hm = QLabel( _('Create a basic news recipe, by adding RSS feeds to it.\n' 'For some news sources, you will have to use the "Switch to advanced mode" ' 'button below to further customize the fetch process.')) hm.setWordWrap(True) l.addRow(hm) self.title = t = QLineEdit(self) l.addRow(_('Recipe &title:'), t) t.setStyleSheet('QLineEdit { font-weight: bold }') self.oldest_article = o = QSpinBox(self) o.setSuffix(' ' + _('day(s)')) o.setToolTip(_("The oldest article to download")) o.setMinimum(1), o.setMaximum(36500) l.addRow(_('&Oldest article:'), o) self.max_articles = m = QSpinBox(self) m.setMinimum(5), m.setMaximum(100) m.setToolTip(_("Maximum number of articles to download per feed.")) l.addRow(_("&Max. number of articles per feed:"), m) self.fg = fg = QGroupBox(self) fg.setTitle(_("Feeds in recipe")) self.feeds = f = QListWidget(self) fg.h = QHBoxLayout(fg) fg.h.addWidget(f) fg.l = QVBoxLayout() self.up_button = b = QToolButton(self) b.setIcon(QIcon(I('arrow-up.png'))) b.setToolTip(_('Move selected feed up')) fg.l.addWidget(b) b.clicked.connect(self.move_up) self.remove_button = b = QToolButton(self) b.setIcon(QIcon(I('list_remove.png'))) b.setToolTip(_('Remove selected feed')) fg.l.addWidget(b) b.clicked.connect(self.remove_feed) self.down_button = b = QToolButton(self) b.setIcon(QIcon(I('arrow-down.png'))) b.setToolTip(_('Move selected feed down')) fg.l.addWidget(b) b.clicked.connect(self.move_down) fg.h.addLayout(fg.l) l.addRow(fg) self.afg = afg = QGroupBox(self) afg.setTitle(_('Add feed to recipe')) afg.l = QFormLayout(afg) afg.l.setFieldGrowthPolicy( QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) self.feed_title = ft = QLineEdit(self) afg.l.addRow(_('&Feed title:'), ft) self.feed_url = fu = QLineEdit(self) afg.l.addRow(_('Feed &URL:'), fu) self.afb = b = QPushButton(QIcon(I('plus.png')), _('&Add feed'), self) b.setToolTip(_('Add this feed to the recipe')) b.clicked.connect(self.add_feed) afg.l.addRow(b) l.addRow(afg)
def setup_ui(self): # {{{ self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowIcon(QIcon(I('column.png'))) self.vl = l = QVBoxLayout(self) self.heading_label = la = QLabel('') l.addWidget(la) self.shortcuts = s = QLabel('') s.setWordWrap(True) s.linkActivated.connect(self.shortcut_activated) text = '<p>' + _('Quick create:') for col, name in [('isbn', _('ISBN')), ('formats', _('Formats')), ('yesno', _('Yes/No')), ('tags', _('Tags')), ('series', ngettext('Series', 'Series', 1)), ('rating', _('Rating')), ('people', _("Names")), ('text', _('Short text'))]: text += ' <a href="col:%s">%s</a>,' % (col, name) text = text[:-1] s.setText(text) l.addWidget(s) self.g = g = QGridLayout() l.addLayout(g) l.addStretch(10) self.button_box = bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self) bb.accepted.connect(self.accept), bb.rejected.connect(self.reject) l.addWidget(bb) def add_row(text, widget): if text is None: f = g.addWidget if isinstance(widget, QWidget) else g.addLayout f(widget, g.rowCount(), 0, 1, -1) return row = g.rowCount() la = QLabel(text) g.addWidget(la, row, 0, 1, 1) if isinstance(widget, QWidget): la.setBuddy(widget) g.addWidget(widget, row, 1, 1, 1) else: widget.setContentsMargins(0, 0, 0, 0) g.addLayout(widget, row, 1, 1, 1) for i in range(widget.count()): w = widget.itemAt(i).widget() if isinstance(w, QWidget): la.setBuddy(w) break return la # Lookup name self.column_name_box = cnb = QLineEdit(self) cnb.setToolTip( _("Used for searching the column. Must contain only digits and lower case letters." )) add_row(_("&Lookup name:"), cnb) # Heading self.column_heading_box = chb = QLineEdit(self) chb.setToolTip( _("Column heading in the library view and category name in the Tag browser" )) add_row(_("Column &heading:"), chb) # Column Type h = QHBoxLayout() self.column_type_box = ctb = QComboBox(self) ctb.setMinimumWidth(70) ctb.setToolTip( _("What kind of information will be kept in the column.")) h.addWidget(ctb) self.use_decorations = ud = QCheckBox(_("Show &checkmarks"), self) ud.setToolTip( _("Show check marks in the GUI. Values of 'yes', 'checked', and 'true'\n" "will show a green check. Values of 'no', 'unchecked', and 'false' will show a red X.\n" "Everything else will show nothing.")) h.addWidget(ud) self.is_names = ins = QCheckBox(_("Contains names"), self) ins.setToolTip( _("Check this box if this column contains names, like the authors column." )) h.addWidget(ins) add_row(_("&Column type:"), h) # Description self.description_box = d = QLineEdit(self) d.setToolTip(_("Optional text describing what this column is for")) add_row(_("D&escription:"), d) # bool formatting h1 = QHBoxLayout() def add_bool_radio_button(txt): b = QRadioButton(txt) b.clicked.connect(partial(self.bool_radio_button_clicked, b)) h1.addWidget(b) return b self.bool_show_icon_button = add_bool_radio_button(_('&Icon')) self.bool_show_text_button = add_bool_radio_button(_('&Text')) self.bool_show_both_button = add_bool_radio_button(_('&Both')) self.bool_button_group = QGroupBox() self.bool_button_group.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.bool_button_group.setLayout(h1) h = QHBoxLayout() h.addWidget(self.bool_button_group) self.bool_button_group_label = la = QLabel( _('Choose whether an icon, text, or both is shown in the book list' )) la.setWordWrap(True) h.addWidget(la) h.setStretch(1, 10) self.bool_show_label = add_row(_('&Show:'), h) # Date/number formatting h = QHBoxLayout() self.format_box = fb = QLineEdit(self) h.addWidget(fb) self.format_default_label = la = QLabel('') la.setOpenExternalLinks(True), la.setWordWrap(True) h.addWidget(la) self.format_label = add_row('', h) # Float number of decimal digits h = QHBoxLayout() self.decimals_box = fb = QSpinBox(self) fb.setRange(1, 9) fb.setValue(2) h.addWidget(fb) self.decimals_default_label = la = QLabel( _('Control the number of decimal digits you can enter when editing this column' )) la.setWordWrap(True) h.addWidget(la) self.decimals_label = add_row(_('Decimals when &editing:'), h) # Template self.composite_box = cb = TemplateLineEditor(self) self.composite_default_label = cdl = QLabel(_("Default: (nothing)")) cb.setToolTip( _("Field template. Uses the same syntax as save templates.")) cdl.setToolTip( _("Similar to save templates. For example, %s") % "{title} {isbn}") h = QHBoxLayout() h.addWidget(cb), h.addWidget(cdl) self.composite_label = add_row(_("&Template:"), h) # Comments properties self.comments_heading_position = ct = QComboBox(self) for k, text in (('hide', _('No heading')), ('above', _('Show heading above the text')), ('side', _('Show heading to the side of the text'))): ct.addItem(text, k) ct.setToolTip( _('Choose whether or not the column heading is shown in the Book\n' 'details panel and, if shown, where')) self.comments_heading_position_label = add_row(_('Column heading:'), ct) self.comments_type = ct = QComboBox(self) for k, text in (('html', 'HTML'), ('short-text', _('Short text, like a title')), ('long-text', _('Plain text')), ('markdown', _('Plain text formatted using markdown'))): ct.addItem(text, k) ct.setToolTip( _('Choose how the data in this column is interpreted.\n' 'This controls how the data is displayed in the Book details panel\n' 'and how it is edited.')) self.comments_type_label = add_row( _('Interpret this column as:') + ' ', ct) # Values for enum type self.enum_box = eb = QLineEdit(self) eb.setToolTip( _("A comma-separated list of permitted values. The empty value is always\n" "included, and is the default. For example, the list 'one,two,three' has\n" "four values, the first of them being the empty value.")) self.enum_default_label = add_row(_("&Values:"), eb) self.enum_colors = ec = QLineEdit(self) ec.setToolTip( _("A list of color names to use when displaying an item. The\n" "list must be empty or contain a color for each value.")) self.enum_colors_label = add_row(_('Colors:'), ec) # Rating allow half stars self.allow_half_stars = ahs = QCheckBox(_('Allow half stars')) ahs.setToolTip(_('Allow half star ratings, for example: ') + '★★★⯨') add_row(None, ahs) # Composite display properties l = QHBoxLayout() self.composite_sort_by_label = la = QLabel(_("&Sort/search column by")) self.composite_sort_by = csb = QComboBox(self) la.setBuddy(csb), csb.setToolTip( _("How this column should handled in the GUI when sorting and searching" )) l.addWidget(la), l.addWidget(csb) self.composite_make_category = cmc = QCheckBox( _("Show in Tag browser")) cmc.setToolTip( _("If checked, this column will appear in the Tag browser as a category" )) l.addWidget(cmc) self.composite_contains_html = cch = QCheckBox( _("Show as HTML in Book details")) cch.setToolTip( '<p>' + _('If checked, this column will be displayed as HTML in ' 'Book details and the Content server. This can be used to ' 'construct links with the template language. For example, ' 'the template ' '<pre><big><b>{title}</b></big>' '{series:| [|}{series_index:| [|]]}</pre>' 'will create a field displaying the title in bold large ' 'characters, along with the series, for example <br>"<big><b>' 'An Oblique Approach</b></big> [Belisarius [1]]". The template ' '<pre><a href="https://www.beam-ebooks.de/ebook/{identifiers' ':select(beam)}">Beam book</a></pre> ' 'will generate a link to the book on the Beam e-books site.') + '</p>') l.addWidget(cch) add_row(None, l) # Default value self.default_value = dv = QLineEdit(self) dv.setToolTip('<p>' + _( 'Default value when a new book is added to the ' 'library. For Date columns enter the word "Now", or the date as ' 'yyyy-mm-dd. For Yes/No columns enter "Yes" or "No". For Text with ' 'a fixed set of values enter one of the permitted values. For ' 'Rating columns enter a number between 0 and 5.') + '</p>') self.default_value_label = add_row(_('&Default value:'), dv) self.resize(self.sizeHint())
class CreateCustomColumn(QDialog): # Note: in this class, we are treating is_multiple as the boolean that # custom_columns expects to find in its structure. It does not use the dict column_types = dict( enumerate(( { 'datatype': 'text', 'text': _('Text, column shown in the Tag browser'), 'is_multiple': False }, { 'datatype': '*text', 'text': _('Comma separated text, like tags, shown in the Tag browser'), 'is_multiple': True }, { 'datatype': 'comments', 'text': _('Long text, like comments, not shown in the Tag browser'), 'is_multiple': False }, { 'datatype': 'series', 'text': _('Text column for keeping series-like information'), 'is_multiple': False }, { 'datatype': 'enumeration', 'text': _('Text, but with a fixed set of permitted values'), 'is_multiple': False }, { 'datatype': 'datetime', 'text': _('Date'), 'is_multiple': False }, { 'datatype': 'float', 'text': _('Floating point numbers'), 'is_multiple': False }, { 'datatype': 'int', 'text': _('Integers'), 'is_multiple': False }, { 'datatype': 'rating', 'text': _('Ratings, shown with stars'), 'is_multiple': False }, { 'datatype': 'bool', 'text': _('Yes/No'), 'is_multiple': False }, { 'datatype': 'composite', 'text': _('Column built from other columns'), 'is_multiple': False }, { 'datatype': '*composite', 'text': _('Column built from other columns, behaves like tags'), 'is_multiple': True }, ))) column_types_map = { k['datatype']: idx for idx, k in iteritems(column_types) } def __init__(self, gui, caller, current_key, standard_colheads, freeze_lookup_name=False): QDialog.__init__(self, gui) self.gui = gui self.setup_ui() self.setWindowTitle(_('Create a custom column')) self.heading_label.setText('<b>' + _('Create a custom column')) # Remove help icon on title bar icon = self.windowIcon() self.setWindowFlags(self.windowFlags() & (~Qt.WindowType.WindowContextHelpButtonHint)) self.setWindowIcon(icon) self.simple_error = partial(error_dialog, self, show=True, show_copy_button=False) for sort_by in [_('Text'), _('Number'), _('Date'), _('Yes/No')]: self.composite_sort_by.addItem(sort_by) self.caller = caller self.caller.cc_column_key = None self.editing_col = current_key is not None self.standard_colheads = standard_colheads self.column_type_box.setMaxVisibleItems(len(self.column_types)) for t in self.column_types: self.column_type_box.addItem(self.column_types[t]['text']) self.column_type_box.currentIndexChanged.connect(self.datatype_changed) if not self.editing_col: self.datatype_changed() self.exec() return self.setWindowTitle(_('Edit custom column')) self.heading_label.setText('<b>' + _('Edit custom column')) self.shortcuts.setVisible(False) col = current_key if col not in caller.custcols: self.simple_error( '', _('The selected column is not a user-defined column')) return c = caller.custcols[col] self.column_name_box.setText(c['label']) if freeze_lookup_name: self.column_name_box.setEnabled(False) self.column_heading_box.setText(c['name']) self.column_heading_box.setFocus() ct = c['datatype'] if c['is_multiple']: ct = '*' + ct self.orig_column_number = c['colnum'] self.orig_column_name = col column_numbers = dict( map(lambda x: (self.column_types[x]['datatype'], x), self.column_types)) self.column_type_box.setCurrentIndex(column_numbers[ct]) self.column_type_box.setEnabled(False) if ct == 'datetime': if c['display'].get('date_format', None): self.format_box.setText(c['display'].get('date_format', '')) elif ct in ['composite', '*composite']: self.composite_box.setText(c['display'].get( 'composite_template', '')) sb = c['display'].get('composite_sort', 'text') vals = ['text', 'number', 'date', 'bool'] if sb in vals: sb = vals.index(sb) else: sb = 0 self.composite_sort_by.setCurrentIndex(sb) self.composite_make_category.setChecked(c['display'].get( 'make_category', False)) self.composite_contains_html.setChecked(c['display'].get( 'contains_html', False)) elif ct == 'enumeration': self.enum_box.setText(','.join(c['display'].get('enum_values', []))) self.enum_colors.setText(','.join(c['display'].get( 'enum_colors', []))) elif ct in ['int', 'float']: if c['display'].get('number_format', None): self.format_box.setText(c['display'].get('number_format', '')) elif ct == 'comments': idx = max( 0, self.comments_heading_position.findData(c['display'].get( 'heading_position', 'hide'))) self.comments_heading_position.setCurrentIndex(idx) idx = max( 0, self.comments_type.findData(c['display'].get( 'interpret_as', 'html'))) self.comments_type.setCurrentIndex(idx) elif ct == 'rating': self.allow_half_stars.setChecked( bool(c['display'].get('allow_half_stars', False))) elif ct == 'bool': icon = bool(c['display'].get('bools_show_icons', True)) txt = bool(c['display'].get('bools_show_text', False)) if icon and txt: self.bool_show_both_button.click() elif icon: self.bool_show_icon_button.click() else: self.bool_show_text_button.click() # Default values dv = c['display'].get('default_value', None) if dv is not None: if ct == 'bool': self.default_value.setText(_('Yes') if dv else _('No')) elif ct == 'datetime': self.default_value.setText(_('Now') if dv == 'now' else dv) elif ct == 'rating': if self.allow_half_stars.isChecked(): self.default_value.setText(str(dv / 2)) else: self.default_value.setText(str(dv // 2)) elif ct in ('int', 'float'): self.default_value.setText(str(dv)) elif ct not in ('composite', '*composite'): self.default_value.setText(dv) self.datatype_changed() if ct in ['text', 'composite', 'enumeration']: self.use_decorations.setChecked(c['display'].get( 'use_decorations', False)) elif ct == '*text': self.is_names.setChecked(c['display'].get('is_names', False)) self.description_box.setText(c['display'].get('description', '')) self.decimals_box.setValue( min(9, max(1, int(c['display'].get('decimals', 2))))) all_colors = [str(s) for s in list(QColor.colorNames())] self.enum_colors_label.setToolTip('<p>' + ', '.join(all_colors) + '</p>') self.exec() def shortcut_activated(self, url): # {{{ which = str(url).split(':')[-1] self.column_type_box.setCurrentIndex({ 'yesno': self.column_types_map['bool'], 'tags': self.column_types_map['*text'], 'series': self.column_types_map['series'], 'rating': self.column_types_map['rating'], 'people': self.column_types_map['*text'], 'text': self.column_types_map['comments'], }.get(which, self.column_types_map['composite'])) self.column_name_box.setText(which) self.column_heading_box.setText({ 'isbn': 'ISBN', 'formats': _('Formats'), 'yesno': _('Yes/No'), 'tags': _('My Tags'), 'series': _('My Series'), 'rating': _('My Rating'), 'people': _('People'), 'text': _('My Title'), }[which]) self.is_names.setChecked(which == 'people') if self.composite_box.isVisible(): self.composite_box.setText({ 'isbn': '{identifiers:select(isbn)}', 'formats': "{:'re(approximate_formats(), ',', ', ')'}", }[which]) self.composite_sort_by.setCurrentIndex(0) if which == 'text': self.comments_heading_position.setCurrentIndex( self.comments_heading_position.findData('side')) self.comments_type.setCurrentIndex( self.comments_type.findData('short-text')) # }}} def setup_ui(self): # {{{ self.setWindowModality(Qt.WindowModality.ApplicationModal) self.setWindowIcon(QIcon(I('column.png'))) self.vl = l = QVBoxLayout(self) self.heading_label = la = QLabel('') l.addWidget(la) self.shortcuts = s = QLabel('') s.setWordWrap(True) s.linkActivated.connect(self.shortcut_activated) text = '<p>' + _('Quick create:') for col, name in [('isbn', _('ISBN')), ('formats', _('Formats')), ('yesno', _('Yes/No')), ('tags', _('Tags')), ('series', ngettext('Series', 'Series', 1)), ('rating', _('Rating')), ('people', _("Names")), ('text', _('Short text'))]: text += ' <a href="col:%s">%s</a>,' % (col, name) text = text[:-1] s.setText(text) l.addWidget(s) self.g = g = QGridLayout() l.addLayout(g) l.addStretch(10) self.button_box = bb = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, self) bb.accepted.connect(self.accept), bb.rejected.connect(self.reject) l.addWidget(bb) def add_row(text, widget): if text is None: f = g.addWidget if isinstance(widget, QWidget) else g.addLayout f(widget, g.rowCount(), 0, 1, -1) return row = g.rowCount() la = QLabel(text) g.addWidget(la, row, 0, 1, 1) if isinstance(widget, QWidget): la.setBuddy(widget) g.addWidget(widget, row, 1, 1, 1) else: widget.setContentsMargins(0, 0, 0, 0) g.addLayout(widget, row, 1, 1, 1) for i in range(widget.count()): w = widget.itemAt(i).widget() if isinstance(w, QWidget): la.setBuddy(w) break return la # Lookup name self.column_name_box = cnb = QLineEdit(self) cnb.setToolTip( _("Used for searching the column. Must contain only digits and lower case letters." )) add_row(_("&Lookup name:"), cnb) # Heading self.column_heading_box = chb = QLineEdit(self) chb.setToolTip( _("Column heading in the library view and category name in the Tag browser" )) add_row(_("Column &heading:"), chb) # Column Type h = QHBoxLayout() self.column_type_box = ctb = QComboBox(self) ctb.setMinimumWidth(70) ctb.setToolTip( _("What kind of information will be kept in the column.")) h.addWidget(ctb) self.use_decorations = ud = QCheckBox(_("Show &checkmarks"), self) ud.setToolTip( _("Show check marks in the GUI. Values of 'yes', 'checked', and 'true'\n" "will show a green check. Values of 'no', 'unchecked', and 'false' will show a red X.\n" "Everything else will show nothing.")) h.addWidget(ud) self.is_names = ins = QCheckBox(_("Contains names"), self) ins.setToolTip( _("Check this box if this column contains names, like the authors column." )) h.addWidget(ins) add_row(_("&Column type:"), h) # Description self.description_box = d = QLineEdit(self) d.setToolTip(_("Optional text describing what this column is for")) add_row(_("D&escription:"), d) # bool formatting h1 = QHBoxLayout() def add_bool_radio_button(txt): b = QRadioButton(txt) b.clicked.connect(partial(self.bool_radio_button_clicked, b)) h1.addWidget(b) return b self.bool_show_icon_button = add_bool_radio_button(_('&Icon')) self.bool_show_text_button = add_bool_radio_button(_('&Text')) self.bool_show_both_button = add_bool_radio_button(_('&Both')) self.bool_button_group = QGroupBox() self.bool_button_group.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.bool_button_group.setLayout(h1) h = QHBoxLayout() h.addWidget(self.bool_button_group) self.bool_button_group_label = la = QLabel( _('Choose whether an icon, text, or both is shown in the book list' )) la.setWordWrap(True) h.addWidget(la) h.setStretch(1, 10) self.bool_show_label = add_row(_('&Show:'), h) # Date/number formatting h = QHBoxLayout() self.format_box = fb = QLineEdit(self) h.addWidget(fb) self.format_default_label = la = QLabel('') la.setOpenExternalLinks(True), la.setWordWrap(True) h.addWidget(la) self.format_label = add_row('', h) # Float number of decimal digits h = QHBoxLayout() self.decimals_box = fb = QSpinBox(self) fb.setRange(1, 9) fb.setValue(2) h.addWidget(fb) self.decimals_default_label = la = QLabel( _('Control the number of decimal digits you can enter when editing this column' )) la.setWordWrap(True) h.addWidget(la) self.decimals_label = add_row(_('Decimals when &editing:'), h) # Template self.composite_box = cb = TemplateLineEditor(self) self.composite_default_label = cdl = QLabel(_("Default: (nothing)")) cb.setToolTip( _("Field template. Uses the same syntax as save templates.")) cdl.setToolTip( _("Similar to save templates. For example, %s") % "{title} {isbn}") h = QHBoxLayout() h.addWidget(cb), h.addWidget(cdl) self.composite_label = add_row(_("&Template:"), h) # Comments properties self.comments_heading_position = ct = QComboBox(self) for k, text in (('hide', _('No heading')), ('above', _('Show heading above the text')), ('side', _('Show heading to the side of the text'))): ct.addItem(text, k) ct.setToolTip( _('Choose whether or not the column heading is shown in the Book\n' 'details panel and, if shown, where')) self.comments_heading_position_label = add_row(_('Column heading:'), ct) self.comments_type = ct = QComboBox(self) for k, text in (('html', 'HTML'), ('short-text', _('Short text, like a title')), ('long-text', _('Plain text')), ('markdown', _('Plain text formatted using markdown'))): ct.addItem(text, k) ct.setToolTip( _('Choose how the data in this column is interpreted.\n' 'This controls how the data is displayed in the Book details panel\n' 'and how it is edited.')) self.comments_type_label = add_row( _('Interpret this column as:') + ' ', ct) # Values for enum type self.enum_box = eb = QLineEdit(self) eb.setToolTip( _("A comma-separated list of permitted values. The empty value is always\n" "included, and is the default. For example, the list 'one,two,three' has\n" "four values, the first of them being the empty value.")) self.enum_default_label = add_row(_("&Values:"), eb) self.enum_colors = ec = QLineEdit(self) ec.setToolTip( _("A list of color names to use when displaying an item. The\n" "list must be empty or contain a color for each value.")) self.enum_colors_label = add_row(_('Colors:'), ec) # Rating allow half stars self.allow_half_stars = ahs = QCheckBox(_('Allow half stars')) ahs.setToolTip(_('Allow half star ratings, for example: ') + '★★★⯨') add_row(None, ahs) # Composite display properties l = QHBoxLayout() self.composite_sort_by_label = la = QLabel(_("&Sort/search column by")) self.composite_sort_by = csb = QComboBox(self) la.setBuddy(csb), csb.setToolTip( _("How this column should handled in the GUI when sorting and searching" )) l.addWidget(la), l.addWidget(csb) self.composite_make_category = cmc = QCheckBox( _("Show in Tag browser")) cmc.setToolTip( _("If checked, this column will appear in the Tag browser as a category" )) l.addWidget(cmc) self.composite_contains_html = cch = QCheckBox( _("Show as HTML in Book details")) cch.setToolTip( '<p>' + _('If checked, this column will be displayed as HTML in ' 'Book details and the Content server. This can be used to ' 'construct links with the template language. For example, ' 'the template ' '<pre><big><b>{title}</b></big>' '{series:| [|}{series_index:| [|]]}</pre>' 'will create a field displaying the title in bold large ' 'characters, along with the series, for example <br>"<big><b>' 'An Oblique Approach</b></big> [Belisarius [1]]". The template ' '<pre><a href="https://www.beam-ebooks.de/ebook/{identifiers' ':select(beam)}">Beam book</a></pre> ' 'will generate a link to the book on the Beam e-books site.') + '</p>') l.addWidget(cch) add_row(None, l) # Default value self.default_value = dv = QLineEdit(self) dv.setToolTip('<p>' + _( 'Default value when a new book is added to the ' 'library. For Date columns enter the word "Now", or the date as ' 'yyyy-mm-dd. For Yes/No columns enter "Yes" or "No". For Text with ' 'a fixed set of values enter one of the permitted values. For ' 'Rating columns enter a number between 0 and 5.') + '</p>') self.default_value_label = add_row(_('&Default value:'), dv) self.resize(self.sizeHint()) # }}} def bool_radio_button_clicked(self, button, clicked): if clicked: self.bool_button_group.setFocusProxy(button) def datatype_changed(self, *args): try: col_type = self.column_types[ self.column_type_box.currentIndex()]['datatype'] except: col_type = None needs_format = col_type in ('datetime', 'int', 'float') for x in ('box', 'default_label', 'label'): getattr(self, 'format_' + x).setVisible(needs_format) getattr(self, 'decimals_' + x).setVisible(col_type == 'float') if needs_format: if col_type == 'datetime': l, dl = _('&Format for dates:'), _('Default: dd MMM yyyy.') self.format_box.setToolTip( _('<p>Date format.</p>' '<p>The formatting codes are:' '<ul>' '<li>d : the day as number without a leading zero (1 to 31)</li>' '<li>dd : the day as number with a leading zero (01 to 31)</li>' '<li>ddd : the abbreviated localized day name (e.g. "Mon" to "Sun").</li>' '<li>dddd : the long localized day name (e.g. "Monday" to "Sunday").</li>' '<li>M : the <b>month</b> as number without a leading zero (1 to 12).</li>' '<li>MM : the <b>month</b> as number with a leading zero (01 to 12)</li>' '<li>MMM : the abbreviated localized <b>month</b> name (e.g. "Jan" to "Dec").</li>' '<li>MMMM : the long localized <b>month</b> name (e.g. "January" to "December").</li>' '<li>yy : the year as two digit number (00 to 99).</li>' '<li>yyyy : the year as four digit number.</li>' '<li>h : the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm)</li>' '<li>hh : the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm)</li>' '<li>m : the <b>minutes</b> without a leading 0 (0 to 59)</li>' '<li>mm : the <b>minutes</b> with a leading 0 (00 to 59)</li>' '<li>s : the seconds without a leading 0 (0 to 59)</li>' '<li>ss : the seconds with a leading 0 (00 to 59)</li>' '<li>ap : use a 12-hour clock instead of a 24-hour clock, with "ap" replaced by the localized string for am or pm</li>' '<li>AP : use a 12-hour clock instead of a 24-hour clock, with "AP" replaced by the localized string for AM or PM</li>' '<li>iso : the date with time and timezone. Must be the only format present</li>' '</ul></p>' "<p>For example:\n" "<ul>\n" "<li>ddd, d MMM yyyy gives Mon, 5 Jan 2010</li>\n" "<li>dd MMMM yy gives 05 January 10</li>\n" "</ul> ")) else: l, dl = _('&Format for numbers:'), ('<p>' + _( 'Default: Not formatted. For format language details see' ' <a href="https://docs.python.org/library/string.html#format-string-syntax">the Python documentation</a>' )) if col_type == 'int': self.format_box.setToolTip('<p>' + _( 'Examples: The format <code>{0:0>4d}</code> ' 'gives a 4-digit number with leading zeros. The format ' '<code>{0:d} days</code> prints the number then the word "days"' ) + '</p>') else: self.format_box.setToolTip('<p>' + _( 'Examples: The format <code>{0:.1f}</code> gives a floating ' 'point number with 1 digit after the decimal point. The format ' '<code>Price: $ {0:,.2f}</code> prints ' '"Price $ " then displays the number with 2 digits ' 'after the decimal point and thousands separated by commas.' ) + '</p>') self.format_label.setText(l), self.format_default_label.setText(dl) for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label', 'make_category', 'contains_html'): getattr(self, 'composite_' + x).setVisible(col_type in ['composite', '*composite']) for x in ('box', 'default_label', 'colors', 'colors_label'): getattr(self, 'enum_' + x).setVisible(col_type == 'enumeration') for x in ('value_label', 'value'): getattr(self, 'default_' + x).setVisible(col_type not in ['composite', '*composite']) self.use_decorations.setVisible( col_type in ['text', 'composite', 'enumeration']) self.is_names.setVisible(col_type == '*text') is_comments = col_type == 'comments' self.comments_heading_position.setVisible(is_comments) self.comments_heading_position_label.setVisible(is_comments) self.comments_type.setVisible(is_comments) self.comments_type_label.setVisible(is_comments) self.allow_half_stars.setVisible(col_type == 'rating') is_bool = col_type == 'bool' self.bool_button_group.setVisible(is_bool) self.bool_button_group_label.setVisible(is_bool) self.bool_show_label.setVisible(is_bool) def accept(self): col = str(self.column_name_box.text()).strip() if not col: return self.simple_error('', _('No lookup name was provided')) if col.startswith('#'): col = col[1:] if re.match(r'^\w*$', col) is None or not col[0].isalpha() or col.lower() != col: return self.simple_error( '', _('The lookup name must contain only ' 'lower case letters, digits and underscores, and start with a letter' )) if col.endswith('_index'): return self.simple_error( '', _('Lookup names cannot end with _index, ' 'because these names are reserved for the index of a series column.' )) col_heading = str(self.column_heading_box.text()).strip() coldef = self.column_types[self.column_type_box.currentIndex()] col_type = coldef['datatype'] if col_type[0] == '*': col_type = col_type[1:] is_multiple = True else: is_multiple = False if not col_heading: return self.simple_error('', _('No column heading was provided')) db = self.gui.library_view.model().db key = db.field_metadata.custom_field_prefix + col cc = self.caller.custcols if key in cc and (not self.editing_col or cc[key]['colnum'] != self.orig_column_number): return self.simple_error( '', _('The lookup name %s is already used') % col) bad_head = False for cc in self.caller.custcols.values(): if cc['name'] == col_heading and cc[ 'colnum'] != self.orig_column_number: bad_head = True break for t in self.standard_colheads: if self.standard_colheads[t] == col_heading: bad_head = True if bad_head: return self.simple_error( '', _('The heading %s is already used') % col_heading) display_dict = {} default_val = (str(self.default_value.text()).strip() if col_type != 'composite' else None) if col_type == 'datetime': if str(self.format_box.text()).strip(): display_dict = { 'date_format': str(self.format_box.text()).strip() } else: display_dict = {'date_format': None} if default_val: if default_val == _('Now'): display_dict['default_value'] = 'now' else: try: tv = parse_date(default_val) except: tv = UNDEFINED_DATE if tv == UNDEFINED_DATE: return self.simple_error( _('Invalid default value'), _('The default value must be "Now" or a date')) display_dict['default_value'] = default_val elif col_type == 'composite': if not str(self.composite_box.text()).strip(): return self.simple_error( '', _('You must enter a template for ' 'composite columns')) display_dict = { 'composite_template': str(self.composite_box.text()).strip(), 'composite_sort': ['text', 'number', 'date', 'bool'][self.composite_sort_by.currentIndex()], 'make_category': self.composite_make_category.isChecked(), 'contains_html': self.composite_contains_html.isChecked(), } elif col_type == 'enumeration': if not str(self.enum_box.text()).strip(): return self.simple_error( '', _('You must enter at least one ' 'value for enumeration columns')) l = [ v.strip() for v in str(self.enum_box.text()).split(',') if v.strip() ] l_lower = [v.lower() for v in l] for i, v in enumerate(l_lower): if v in l_lower[i + 1:]: return self.simple_error( '', _('The value "{0}" is in the ' 'list more than once, perhaps with different case'). format(l[i])) c = str(self.enum_colors.text()) if c: c = [ v.strip() for v in str(self.enum_colors.text()).split(',') ] else: c = [] if len(c) != 0 and len(c) != len(l): return self.simple_error( '', _('The colors box must be empty or ' 'contain the same number of items as the value box')) for tc in c: if tc not in QColor.colorNames() and not re.match( "#(?:[0-9a-f]{3}){1,4}", tc, re.I): return self.simple_error( '', _('The color {0} is unknown').format(tc)) display_dict = {'enum_values': l, 'enum_colors': c} if default_val: if default_val not in l: return self.simple_error( _('Invalid default value'), _('The default value must be one of the permitted values' )) display_dict['default_value'] = default_val elif col_type == 'text' and is_multiple: display_dict = {'is_names': self.is_names.isChecked()} elif col_type in ['int', 'float']: if str(self.format_box.text()).strip(): display_dict = { 'number_format': str(self.format_box.text()).strip() } else: display_dict = {'number_format': None} if col_type == 'float': display_dict['decimals'] = int(self.decimals_box.value()) if default_val: try: if col_type == 'int': msg = _('The default value must be an integer') tv = int(default_val) display_dict['default_value'] = tv else: msg = _('The default value must be a real number') tv = float(default_val) display_dict['default_value'] = tv except: return self.simple_error(_('Invalid default value'), msg) elif col_type == 'comments': display_dict['heading_position'] = str( self.comments_heading_position.currentData()) display_dict['interpret_as'] = str( self.comments_type.currentData()) elif col_type == 'rating': half_stars = bool(self.allow_half_stars.isChecked()) display_dict['allow_half_stars'] = half_stars if default_val: try: tv = int((float(default_val) if half_stars else int(default_val)) * 2) except: tv = -1 if tv < 0 or tv > 10: if half_stars: return self.simple_error( _('Invalid default value'), _('The default value must be a real number between 0 and 5.0' )) else: return self.simple_error( _('Invalid default value'), _('The default value must be an integer between 0 and 5' )) display_dict['default_value'] = tv elif col_type == 'bool': if default_val: tv = {_('Yes'): True, _('No'): False}.get(default_val, None) if tv is None: return self.simple_error( _('Invalid default value'), _('The default value must be "Yes" or "No"')) display_dict['default_value'] = tv show_icon = bool(self.bool_show_icon_button.isChecked()) or bool( self.bool_show_both_button.isChecked()) show_text = bool(self.bool_show_text_button.isChecked()) or bool( self.bool_show_both_button.isChecked()) display_dict['bools_show_text'] = show_text display_dict['bools_show_icons'] = show_icon if col_type in ['text', 'composite', 'enumeration' ] and not is_multiple: display_dict['use_decorations'] = self.use_decorations.checkState( ) == Qt.CheckState.Checked if default_val and 'default_value' not in display_dict: display_dict['default_value'] = default_val display_dict['description'] = self.description_box.text().strip() if not self.editing_col: self.caller.custcols[key] = { 'label': col, 'name': col_heading, 'datatype': col_type, 'display': display_dict, 'normalized': None, 'colnum': None, 'is_multiple': is_multiple, } self.caller.cc_column_key = key else: cc = self.caller.custcols[self.orig_column_name] cc['label'] = col cc['name'] = col_heading # Remove any previous default value cc['display'].pop('default_value', None) cc['display'].update(display_dict) cc['*edited'] = True cc['*must_restart'] = True self.caller.cc_column_key = key QDialog.accept(self) def reject(self): QDialog.reject(self)