diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 8175e6cbac..d42f6b9da0 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -694,7 +694,16 @@ class Behavior(PreferencesPlugin): name_order = 2 config_widget = 'calibre.gui2.preferences.behavior' -plugins += [LookAndFeel, Behavior] +class Columns(PreferencesPlugin): + name = 'Custom Columns' + gui_name = _('Add your own columns') + category = 'Interface' + gui_category = _('Interface') + category_order = 1 + name_order = 3 + config_widget = 'calibre.gui2.preferences.columns' + +plugins += [LookAndFeel, Behavior, Columns] #}}} diff --git a/src/calibre/gui2/preferences/columns.py b/src/calibre/gui2/preferences/columns.py new file mode 100644 index 0000000000..973e214c2f --- /dev/null +++ b/src/calibre/gui2/preferences/columns.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import copy, sys + +from PyQt4.Qt import Qt, QVariant, QListWidgetItem + +from calibre.gui2.preferences import ConfigWidgetBase, test_widget +from calibre.gui2.preferences.columns_ui import Ui_Form +from calibre.gui2.preferences.create_custom_column import CreateCustomColumn +from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + db = self.gui.library_view.model().db + self.custcols = copy.deepcopy(db.field_metadata.get_custom_field_metadata()) + + self.column_up.clicked.connect(self.up_column) + self.column_down.clicked.connect(self.down_column) + self.del_custcol_button.clicked.connect(self.del_custcol) + self.add_custcol_button.clicked.connect(self.add_custcol) + self.add_col_button.clicked.connect(self.add_custcol) + self.edit_custcol_button.clicked.connect(self.edit_custcol) + for signal in ('Activated', 'Changed', 'DoubleClicked', 'Clicked'): + signal = getattr(self.opt_columns, 'item'+signal) + signal.connect(self.columns_changed) + + def initialize(self): + ConfigWidgetBase.initialize(self) + self.init_columns() + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + self.init_columns(defaults=True) + self.changed_signal.emit() + + def commit(self): + rr = ConfigWidgetBase.commit(self) + return self.apply_custom_column_changes() or rr + + def columns_changed(self, *args): + self.changed_signal.emit() + + def columns_state(self, defaults=False): + if defaults: + return self.gui.library_view.get_default_state() + return self.gui.library_view.get_state() + + def init_columns(self, defaults=False): + # Set up columns + self.opt_columns.blockSignals(True) + model = self.gui.library_view.model() + colmap = list(model.column_map) + state = self.columns_state(defaults) + hidden_cols = state['hidden_columns'] + positions = state['column_positions'] + colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y])) + self.opt_columns.clear() + for col in colmap: + item = QListWidgetItem(model.headers[col], self.opt_columns) + item.setData(Qt.UserRole, QVariant(col)) + flags = Qt.ItemIsEnabled|Qt.ItemIsSelectable + if col != 'ondevice': + flags |= Qt.ItemIsUserCheckable + item.setFlags(flags) + if col != 'ondevice': + item.setCheckState(Qt.Unchecked if col in hidden_cols else + Qt.Checked) + self.opt_columns.blockSignals(False) + + def up_column(self): + idx = self.opt_columns.currentRow() + if idx > 0: + self.opt_columns.insertItem(idx-1, self.opt_columns.takeItem(idx)) + self.opt_columns.setCurrentRow(idx-1) + self.changed_signal.emit() + + def down_column(self): + idx = self.opt_columns.currentRow() + if idx < self.opt_columns.count()-1: + self.opt_columns.insertItem(idx+1, self.opt_columns.takeItem(idx)) + self.opt_columns.setCurrentRow(idx+1) + self.changed_signal.emit() + + def del_custcol(self): + idx = self.opt_columns.currentRow() + if idx < 0: + return error_dialog(self, '', _('You must select a column to delete it'), + show=True) + col = unicode(self.opt_columns.item(idx).data(Qt.UserRole).toString()) + if col not in self.custcols: + return error_dialog(self, '', + _('The selected column is not a custom column'), show=True) + if not question_dialog(self, _('Are you sure?'), + _('Do you really want to delete column %s and all its data?') % + self.custcols[col]['name'], show_copy_button=False): + return + self.opt_columns.item(idx).setCheckState(False) + self.opt_columns.takeItem(idx) + self.custcols[col]['*deleteme'] = True + self.changed_signal.emit() + + def add_custcol(self): + model = self.gui.library_view.model() + CreateCustomColumn(self, False, model.orig_headers, ALL_COLUMNS) + self.changed_signal.emit() + + def edit_custcol(self): + model = self.gui.library_view.model() + CreateCustomColumn(self, True, model.orig_headers, ALL_COLUMNS) + self.changed_signal.emit() + + def apply_custom_column_changes(self): + model = self.gui.library_view.model() + db = model.db + config_cols = [unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\ + for i in range(self.opt_columns.count())] + if not config_cols: + config_cols = ['title'] + removed_cols = set(model.column_map) - set(config_cols) + hidden_cols = set([unicode(self.opt_columns.item(i).data(Qt.UserRole).toString())\ + for i in range(self.opt_columns.count()) \ + if self.opt_columns.item(i).checkState()==Qt.Unchecked]) + hidden_cols = hidden_cols.union(removed_cols) # Hide removed cols + hidden_cols = list(hidden_cols.intersection(set(model.column_map))) + if 'ondevice' in hidden_cols: + hidden_cols.remove('ondevice') + def col_pos(x, y): + xidx = config_cols.index(x) if x in config_cols else sys.maxint + yidx = config_cols.index(y) if y in config_cols else sys.maxint + return cmp(xidx, yidx) + positions = {} + for i, col in enumerate((sorted(model.column_map, cmp=col_pos))): + positions[col] = i + state = {'hidden_columns': hidden_cols, 'column_positions':positions} + self.gui.library_view.apply_state(state) + self.gui.library_view.save_state() + + must_restart = False + for c in self.custcols: + if self.custcols[c]['colnum'] is None: + db.create_custom_column( + label=self.custcols[c]['label'], + name=self.custcols[c]['name'], + datatype=self.custcols[c]['datatype'], + is_multiple=self.custcols[c]['is_multiple'], + display = self.custcols[c]['display']) + must_restart = True + elif '*deleteme' in self.custcols[c]: + db.delete_custom_column(label=self.custcols[c]['label']) + must_restart = True + elif '*edited' in self.custcols[c]: + cc = self.custcols[c] + db.set_custom_column_metadata(cc['colnum'], name=cc['name'], + label=cc['label'], + display = self.custcols[c]['display']) + if '*must_restart' in self.custcols[c]: + must_restart = True + return must_restart + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Interface', 'Custom Columns') + diff --git a/src/calibre/gui2/preferences/custom_columns.ui b/src/calibre/gui2/preferences/columns.ui similarity index 97% rename from src/calibre/gui2/preferences/custom_columns.ui rename to src/calibre/gui2/preferences/columns.ui index 3f26838a07..58d54e48f5 100644 --- a/src/calibre/gui2/preferences/custom_columns.ui +++ b/src/calibre/gui2/preferences/columns.ui @@ -25,7 +25,7 @@ - + true @@ -155,7 +155,7 @@ - + Add &custom column diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py new file mode 100644 index 0000000000..9cad1293a9 --- /dev/null +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -0,0 +1,174 @@ +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' + +'''Dialog to create a new custom column''' + +import re +from functools import partial + +from PyQt4.QtCore import SIGNAL +from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant + +from calibre.gui2.preferences.create_custom_column_ui import Ui_QCreateCustomColumn +from calibre.gui2 import error_dialog + +class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): + + column_types = { + 0:{'datatype':'text', + 'text':_('Text, column shown in the tag browser'), + 'is_multiple':False}, + 1:{'datatype':'*text', + 'text':_('Comma separated text, like tags, shown in the tag browser'), + 'is_multiple':True}, + 2:{'datatype':'comments', + 'text':_('Long text, like comments, not shown in the tag browser'), + 'is_multiple':False}, + 3:{'datatype':'series', + 'text':_('Text column for keeping series-like information'), + 'is_multiple':False}, + 4:{'datatype':'datetime', + 'text':_('Date'), 'is_multiple':False}, + 5:{'datatype':'float', + 'text':_('Floating point numbers'), 'is_multiple':False}, + 6:{'datatype':'int', + 'text':_('Integers'), 'is_multiple':False}, + 7:{'datatype':'rating', + 'text':_('Ratings, shown with stars'), + 'is_multiple':False}, + 8:{'datatype':'bool', + 'text':_('Yes/No'), 'is_multiple':False}, + } + + def __init__(self, parent, editing, standard_colheads, standard_colnames): + QDialog.__init__(self, parent) + Ui_QCreateCustomColumn.__init__(self) + self.setupUi(self) + # Remove help icon on title bar + icon = self.windowIcon() + self.setWindowFlags(self.windowFlags()&(~Qt.WindowContextHelpButtonHint)) + self.setWindowIcon(icon) + + self.simple_error = partial(error_dialog, self, show=True, + show_copy_button=False) + self.connect(self.button_box, SIGNAL("accepted()"), self.accept) + self.connect(self.button_box, SIGNAL("rejected()"), self.reject) + self.parent = parent + self.editing_col = editing + self.standard_colheads = standard_colheads + self.standard_colnames = standard_colnames + 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 + idx = parent.opt_columns.currentRow() + if idx < 0: + self.simple_error(_('No column selected'), + _('No column has been selected')) + return + col = unicode(parent.opt_columns.item(idx).data(Qt.UserRole).toString()) + if col not in parent.custcols: + self.simple_error('', _('Selected column is not a user-defined column')) + return + + c = parent.custcols[col] + self.column_name_box.setText(c['label']) + self.column_heading_box.setText(c['name']) + ct = c['datatype'] if not c['is_multiple'] else '*text' + 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.date_format_box.setText(c['display'].get('date_format', '')) + self.datatype_changed() + self.exec_() + + def datatype_changed(self, *args): + try: + col_type = self.column_types[self.column_type_box.currentIndex()]['datatype'] + except: + col_type = None + df_visible = col_type == 'datetime' + for x in ('box', 'default_label', 'label'): + getattr(self, 'date_format_'+x).setVisible(df_visible) + + + def accept(self): + col = unicode(self.column_name_box.text()) + if not col: + return self.simple_error('', _('No lookup name was provided')) + if re.match('^\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 = unicode(self.column_heading_box.text()) + col_type = self.column_types[self.column_type_box.currentIndex()]['datatype'] + if col_type == '*text': + col_type='text' + is_multiple = True + else: + is_multiple = False + if not col_heading: + return self.simple_error('', _('No column heading was provided')) + bad_col = False + if col in self.parent.custcols: + if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number: + bad_col = True + if bad_col: + return self.simple_error('', _('The lookup name %s is already used')%col) + bad_head = False + for t in self.parent.custcols: + if self.parent.custcols[t]['name'] == col_heading: + if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number: + bad_head = True + 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) + + date_format = {} + if col_type == 'datetime': + if self.date_format_box.text(): + date_format = {'date_format':unicode(self.date_format_box.text())} + else: + date_format = {'date_format': None} + + db = self.parent.gui.library_view.model().db + key = db.field_metadata.custom_field_prefix+col + if not self.editing_col: + db.field_metadata + self.parent.custcols[key] = { + 'label':col, + 'name':col_heading, + 'datatype':col_type, + 'editable':True, + 'display':date_format, + 'normalized':None, + 'colnum':None, + 'is_multiple':is_multiple, + } + item = QListWidgetItem(col_heading, self.parent.opt_columns) + item.setData(Qt.UserRole, QVariant(key)) + item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) + item.setCheckState(Qt.Checked) + else: + idx = self.parent.opt_columns.currentRow() + item = self.parent.opt_columns.item(idx) + item.setData(Qt.UserRole, QVariant(key)) + item.setText(col_heading) + self.parent.custcols[self.orig_column_name]['label'] = col + self.parent.custcols[self.orig_column_name]['name'] = col_heading + self.parent.custcols[self.orig_column_name]['display'].update(date_format) + self.parent.custcols[self.orig_column_name]['*edited'] = True + self.parent.custcols[self.orig_column_name]['*must_restart'] = True + QDialog.accept(self) + + def reject(self): + QDialog.reject(self) diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui new file mode 100644 index 0000000000..5cb9494845 --- /dev/null +++ b/src/calibre/gui2/preferences/create_custom_column.ui @@ -0,0 +1,191 @@ + + + QCreateCustomColumn + + + Qt::ApplicationModal + + + + 0 + 0 + 528 + 199 + + + + + 0 + 0 + + + + Create or edit custom columns + + + + + + QLayout::SetDefaultConstraint + + + 5 + + + + + 0 + + + + + &Lookup name + + + column_name_box + + + + + + + Column &heading + + + column_heading_box + + + + + + + + 20 + 0 + + + + Used for searching the column. Must contain only digits and lower case letters. + + + + + + + Column heading in the library view and category name in the tag browser + + + + + + + Column &type + + + column_type_box + + + + + + + + 0 + 0 + + + + + 70 + 0 + + + + What kind of information will be kept in the column. + + + + + + + + + + 0 + 0 + + + + <p>Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.</p> +<p>For example: +<ul> +<li> ddd, d MMM yyyy gives Mon, 5 Jan 2010<li> +<li>dd MMMM yy gives 05 January 10</li> +</ul> + + + + + + + Use MMM yyyy for month + year, yyyy for year only + + + Default: dd MMM yyyy. + + + + + + + + + Format for &dates + + + date_format_box + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + 75 + true + + + + Create or edit custom columns + + + + + + + + + column_name_box + column_heading_box + column_type_box + date_format_box + button_box + + + +