mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Start implementing a python console in Qt. Launch with calibre-debug -p. Also more work on S&R dialog
This commit is contained in:
commit
2ed30d71d2
@ -37,6 +37,8 @@ Run an embedded python interpreter.
|
||||
parser.add_option('--reinitialize-db', default=None,
|
||||
help='Re-initialize the sqlite calibre database at the '
|
||||
'specified path. Useful to recover from db corruption.')
|
||||
parser.add_option('-p', '--py-console', help='Run python console',
|
||||
default=False, action='store_true')
|
||||
|
||||
return parser
|
||||
|
||||
@ -148,6 +150,9 @@ def main(args=sys.argv):
|
||||
if len(args) > 1:
|
||||
vargs.append(args[-1])
|
||||
main(vargs)
|
||||
elif opts.py_console:
|
||||
from calibre.utils.pyconsole.main import main
|
||||
main()
|
||||
elif opts.command:
|
||||
sys.argv = args[:1]
|
||||
exec opts.command
|
||||
|
@ -11,11 +11,11 @@ from PyQt4 import QtGui
|
||||
|
||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||
from calibre.ebooks.metadata import string_to_authors, \
|
||||
authors_to_string, MetaInformation
|
||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
||||
from calibre.gui2.custom_column_widgets import populate_metadata_page
|
||||
from calibre.gui2.dialogs.progress import BlockingBusy
|
||||
from calibre.gui2 import error_dialog, Dispatcher
|
||||
from calibre.utils.config import dynamic
|
||||
|
||||
class Worker(Thread):
|
||||
|
||||
@ -122,12 +122,20 @@ def format_composite(x, mi):
|
||||
|
||||
class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
|
||||
s_r_functions = {
|
||||
'' : lambda x: x,
|
||||
_('Lower Case') : lambda x: x.lower(),
|
||||
_('Upper Case') : lambda x: x.upper(),
|
||||
_('Title Case') : lambda x: x.title(),
|
||||
}
|
||||
s_r_functions = { '' : lambda x: x,
|
||||
_('Lower Case') : lambda x: x.lower(),
|
||||
_('Upper Case') : lambda x: x.upper(),
|
||||
_('Title Case') : lambda x: x.title(),
|
||||
}
|
||||
|
||||
s_r_match_modes = [ _('Character match'),
|
||||
_('Regular Expression'),
|
||||
]
|
||||
|
||||
s_r_replace_modes = [ _('Replace field'),
|
||||
_('Prepend to field'),
|
||||
_('Append to field'),
|
||||
]
|
||||
|
||||
def __init__(self, window, rows, db):
|
||||
QDialog.__init__(self, window)
|
||||
@ -179,79 +187,123 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
fields.sort()
|
||||
self.search_field.addItems(fields)
|
||||
self.search_field.setMaxVisibleItems(min(len(fields), 20))
|
||||
self.destination_field.addItems(fields)
|
||||
self.destination_field.setMaxVisibleItems(min(len(fields), 20))
|
||||
offset = 10
|
||||
self.s_r_number_of_books = min(7, len(self.ids))
|
||||
for i in range(1,self.s_r_number_of_books+1):
|
||||
w = QtGui.QLabel(self.tabWidgetPage3)
|
||||
w.setText(_('Book %d:')%i)
|
||||
self.gridLayout1.addWidget(w, i+offset, 0, 1, 1)
|
||||
self.testgrid.addWidget(w, i+offset, 0, 1, 1)
|
||||
w = QtGui.QLineEdit(self.tabWidgetPage3)
|
||||
w.setReadOnly(True)
|
||||
name = 'book_%d_text'%i
|
||||
setattr(self, name, w)
|
||||
self.book_1_text.setObjectName(name)
|
||||
self.gridLayout1.addWidget(w, i+offset, 1, 1, 1)
|
||||
self.testgrid.addWidget(w, i+offset, 1, 1, 1)
|
||||
w = QtGui.QLineEdit(self.tabWidgetPage3)
|
||||
w.setReadOnly(True)
|
||||
name = 'book_%d_result'%i
|
||||
setattr(self, name, w)
|
||||
self.book_1_text.setObjectName(name)
|
||||
self.gridLayout1.addWidget(w, i+offset, 2, 1, 1)
|
||||
self.testgrid.addWidget(w, i+offset, 2, 1, 1)
|
||||
|
||||
self.s_r_heading.setText('<p>'+
|
||||
_('Search and replace in text fields using '
|
||||
'regular expressions. The search text is an '
|
||||
'arbitrary python-compatible regular expression. '
|
||||
'The replacement text can contain backreferences '
|
||||
'to parenthesized expressions in the pattern. '
|
||||
'The search is not anchored, and can match and '
|
||||
'replace multiple times on the same string. See '
|
||||
'<a href="http://docs.python.org/library/re.html"> '
|
||||
'this reference</a> '
|
||||
'for more information, and in particular the \'sub\' '
|
||||
'function.') + '<p>' + _(
|
||||
'Note: <b>you can destroy your library</b> '
|
||||
'using this feature. Changes are permanent. There '
|
||||
'is no undo function. You are strongly encouraged '
|
||||
'to back up your library before proceeding.'))
|
||||
self.s_r_heading.setText('<p>'+ _(
|
||||
'<b>You can destroy your library using this feature.</b> '
|
||||
'Changes are permanent. There is no undo function. '
|
||||
' This feature is experimental, and there may be bugs. '
|
||||
'You are strongly encouraged to back up your library '
|
||||
'before proceeding.'
|
||||
) + '<p>' + _(
|
||||
'Search and replace in text fields using character matching '
|
||||
'or regular expressions. In character mode, search text '
|
||||
'found in the specified field is replaced with replace '
|
||||
'text. In regular expression mode, the search text is an '
|
||||
'arbitrary python-compatible regular expression. The '
|
||||
'replacement text can contain backreferences to parenthesized '
|
||||
'expressions in the pattern. The search is not anchored, '
|
||||
'and can match and replace multiple times on the same string. '
|
||||
'See <a href="http://docs.python.org/library/re.html"> '
|
||||
'this reference</a> for more information, and in particular '
|
||||
'the \'sub\' function.'
|
||||
))
|
||||
self.search_mode.addItems(self.s_r_match_modes)
|
||||
self.search_mode.setCurrentIndex(dynamic.get('s_r_search_mode', 0))
|
||||
self.replace_mode.addItems(self.s_r_replace_modes)
|
||||
self.replace_mode.setCurrentIndex(0)
|
||||
|
||||
self.s_r_search_mode = 0
|
||||
self.s_r_error = None
|
||||
self.s_r_obj = None
|
||||
|
||||
self.replace_func.addItems(sorted(self.s_r_functions.keys()))
|
||||
self.search_field.currentIndexChanged[str].connect(self.s_r_field_changed)
|
||||
self.search_mode.currentIndexChanged[int].connect(self.s_r_search_mode_changed)
|
||||
self.search_field.currentIndexChanged[str].connect(self.s_r_search_field_changed)
|
||||
self.destination_field.currentIndexChanged[str].connect(self.s_r_destination_field_changed)
|
||||
|
||||
self.replace_mode.currentIndexChanged[int].connect(self.s_r_paint_results)
|
||||
self.replace_func.currentIndexChanged[str].connect(self.s_r_paint_results)
|
||||
self.search_for.editTextChanged[str].connect(self.s_r_paint_results)
|
||||
self.replace_with.editTextChanged[str].connect(self.s_r_paint_results)
|
||||
self.test_text.editTextChanged[str].connect(self.s_r_paint_results)
|
||||
self.comma_separated.stateChanged.connect(self.s_r_paint_results)
|
||||
self.case_sensitive.stateChanged.connect(self.s_r_paint_results)
|
||||
self.central_widget.setCurrentIndex(0)
|
||||
|
||||
self.search_for.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||
self.replace_with.completer().setCaseSensitivity(Qt.CaseSensitive)
|
||||
|
||||
self.s_r_search_mode_changed(self.search_mode.currentIndex())
|
||||
|
||||
def s_r_field_changed(self, txt):
|
||||
def s_r_get_field(self, mi, field):
|
||||
if field:
|
||||
fm = self.db.metadata_for_field(field)
|
||||
val = mi.get(field, None)
|
||||
if val is None:
|
||||
val = []
|
||||
elif not fm['is_multiple']:
|
||||
val = [val]
|
||||
elif field == 'authors':
|
||||
val = [v.replace(',', '|') for v in val]
|
||||
else:
|
||||
val = []
|
||||
return val
|
||||
|
||||
def s_r_search_field_changed(self, txt):
|
||||
txt = unicode(txt)
|
||||
for i in range(0, self.s_r_number_of_books):
|
||||
if txt:
|
||||
fm = self.db.field_metadata[txt]
|
||||
id = self.ids[i]
|
||||
val = self.db.get_property(id, index_is_id=True,
|
||||
loc=fm['rec_index'])
|
||||
if val is None:
|
||||
val = ''
|
||||
if fm['is_multiple']:
|
||||
val = [t.strip() for t in val.split(fm['is_multiple']) if t.strip()]
|
||||
if val:
|
||||
val.sort(cmp=lambda x,y: cmp(x.lower(), y.lower()))
|
||||
val = val[0]
|
||||
if txt == 'authors':
|
||||
val = val.replace('|', ',')
|
||||
else:
|
||||
val = ''
|
||||
else:
|
||||
val = ''
|
||||
w = getattr(self, 'book_%d_text'%(i+1))
|
||||
w.setText(val)
|
||||
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
|
||||
src = unicode(self.search_field.currentText())
|
||||
t = self.s_r_get_field(mi, src)
|
||||
w.setText(''.join(t[0:1]))
|
||||
self.s_r_paint_results(None)
|
||||
|
||||
def s_r_destination_field_changed(self, txt):
|
||||
txt = unicode(txt)
|
||||
self.comma_separated.setEnabled(True)
|
||||
if txt:
|
||||
fm = self.db.metadata_for_field(txt)
|
||||
if fm['is_multiple']:
|
||||
self.comma_separated.setEnabled(False)
|
||||
self.comma_separated.setChecked(True)
|
||||
self.s_r_paint_results(None)
|
||||
|
||||
def s_r_search_mode_changed(self, val):
|
||||
if val == 0:
|
||||
self.destination_field.setCurrentIndex(0)
|
||||
self.destination_field.setVisible(False)
|
||||
self.destination_field_label.setVisible(False)
|
||||
self.replace_mode.setCurrentIndex(0)
|
||||
self.replace_mode.setVisible(False)
|
||||
self.replace_mode_label.setVisible(False)
|
||||
self.comma_separated.setVisible(False)
|
||||
else:
|
||||
self.destination_field.setVisible(True)
|
||||
self.destination_field_label.setVisible(True)
|
||||
self.replace_mode.setVisible(True)
|
||||
self.replace_mode_label.setVisible(True)
|
||||
self.comma_separated.setVisible(True)
|
||||
self.s_r_paint_results(None)
|
||||
|
||||
def s_r_set_colors(self):
|
||||
@ -265,32 +317,73 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
for i in range(0,self.s_r_number_of_books):
|
||||
getattr(self, 'book_%d_result'%(i+1)).setText('')
|
||||
|
||||
field_match_re = re.compile(r'(^|[^\\])(\\g<)([^>]+)(>)')
|
||||
|
||||
def s_r_func(self, match):
|
||||
rfunc = self.s_r_functions[unicode(self.replace_func.currentText())]
|
||||
rtext = unicode(self.replace_with.text())
|
||||
mi_data = self.mi.all_non_none_fields()
|
||||
|
||||
def fm_func(m):
|
||||
try:
|
||||
if m.group(3) not in self.mi.all_field_keys(): return m.group(0)
|
||||
else: return '%s{%s}'%(m.group(1), m.group(3))
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return m.group(0)
|
||||
|
||||
rtext = re.sub(self.field_match_re, fm_func, rtext)
|
||||
rtext = match.expand(rtext)
|
||||
rtext = format_composite(rtext, mi_data)
|
||||
return rfunc(rtext)
|
||||
|
||||
def s_r_do_regexp(self, mi):
|
||||
src_field = unicode(self.search_field.currentText())
|
||||
src = self.s_r_get_field(mi, src_field)
|
||||
result = []
|
||||
rfunc = self.s_r_functions[unicode(self.replace_func.currentText())]
|
||||
for s in src:
|
||||
t = self.s_r_obj.sub(self.s_r_func, s)
|
||||
if self.search_mode.currentIndex() == 0:
|
||||
t = rfunc(t)
|
||||
result.append(t)
|
||||
return result
|
||||
|
||||
def s_r_do_destination(self, mi, val):
|
||||
src = unicode(self.search_field.currentText())
|
||||
if src == '':
|
||||
return ''
|
||||
dest = unicode(self.destination_field.currentText())
|
||||
if dest == '':
|
||||
dest = src
|
||||
dest_mode = self.replace_mode.currentIndex()
|
||||
|
||||
if dest_mode != 0:
|
||||
dest_val = mi.get(dest, '')
|
||||
if dest_val is None:
|
||||
dest_val = []
|
||||
elif isinstance(dest_val, list):
|
||||
if dest == 'authors':
|
||||
dest_val = [v.replace(',', '|') for v in dest_val]
|
||||
else:
|
||||
dest_val = [dest_val]
|
||||
else:
|
||||
dest_val = []
|
||||
|
||||
if len(val) > 0:
|
||||
if src == 'authors':
|
||||
val = [v.replace(',', '|') for v in val]
|
||||
if dest_mode == 1:
|
||||
val.extend(dest_val)
|
||||
elif dest_mode == 2:
|
||||
val[0:0] = dest_val
|
||||
return val
|
||||
|
||||
def s_r_replace_mode_separator(self):
|
||||
if self.comma_separated.isChecked():
|
||||
return ','
|
||||
return ''
|
||||
|
||||
def s_r_paint_results(self, txt):
|
||||
self.s_r_error = None
|
||||
self.s_r_set_colors()
|
||||
|
||||
if self.case_sensitive.isChecked():
|
||||
flags = 0
|
||||
else:
|
||||
flags = re.I
|
||||
|
||||
try:
|
||||
self.s_r_obj = re.compile(unicode(self.search_for.text()))
|
||||
if self.search_mode.currentIndex() == 0:
|
||||
self.s_r_obj = re.compile(re.escape(unicode(self.search_for.text())), flags)
|
||||
else:
|
||||
self.s_r_obj = re.compile(unicode(self.search_for.text()), flags)
|
||||
except Exception as e:
|
||||
self.s_r_obj = None
|
||||
self.s_r_error = e
|
||||
@ -298,7 +391,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
return
|
||||
|
||||
try:
|
||||
self.mi = MetaInformation(None, None)
|
||||
self.test_result.setText(self.s_r_obj.sub(self.s_r_func,
|
||||
unicode(self.test_text.text())))
|
||||
except Exception as e:
|
||||
@ -307,62 +399,56 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
||||
return
|
||||
|
||||
for i in range(0,self.s_r_number_of_books):
|
||||
id = self.ids[i]
|
||||
self.mi = self.db.get_metadata(id, index_is_id=True)
|
||||
wt = getattr(self, 'book_%d_text'%(i+1))
|
||||
mi = self.db.get_metadata(self.ids[i], index_is_id=True)
|
||||
wr = getattr(self, 'book_%d_result'%(i+1))
|
||||
try:
|
||||
wr.setText(self.s_r_obj.sub(self.s_r_func, unicode(wt.text())))
|
||||
result = self.s_r_do_regexp(mi)
|
||||
t = self.s_r_do_destination(mi, result[0:1])
|
||||
t = self.s_r_replace_mode_separator().join(t)
|
||||
wr.setText(t)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
self.s_r_error = e
|
||||
self.s_r_set_colors()
|
||||
break
|
||||
|
||||
def do_search_replace(self):
|
||||
field = unicode(self.search_field.currentText())
|
||||
if not field or not self.s_r_obj:
|
||||
source = unicode(self.search_field.currentText())
|
||||
if not source or not self.s_r_obj:
|
||||
return
|
||||
|
||||
fm = self.db.field_metadata[field]
|
||||
|
||||
def apply_pattern(val):
|
||||
try:
|
||||
return self.s_r_obj.sub(self.s_r_func, val)
|
||||
except:
|
||||
return val
|
||||
dest = unicode(self.destination_field.currentText())
|
||||
if not dest:
|
||||
dest = source
|
||||
dfm = self.db.field_metadata[dest]
|
||||
|
||||
for id in self.ids:
|
||||
val = self.db.get_property(id, index_is_id=True,
|
||||
loc=fm['rec_index'])
|
||||
mi = self.db.get_metadata(id, index_is_id=True,)
|
||||
val = mi.get(source)
|
||||
if val is None:
|
||||
continue
|
||||
if fm['is_multiple']:
|
||||
res = []
|
||||
for val in [t.strip() for t in val.split(fm['is_multiple'])]:
|
||||
v = apply_pattern(val).strip()
|
||||
if v:
|
||||
res.append(v)
|
||||
val = res
|
||||
if fm['is_custom']:
|
||||
val = self.s_r_do_regexp(mi)
|
||||
val = self.s_r_do_destination(mi, val)
|
||||
if dfm['is_multiple']:
|
||||
if dfm['is_custom']:
|
||||
# The standard tags and authors values want to be lists.
|
||||
# All custom columns are to be strings
|
||||
val = fm['is_multiple'].join(val)
|
||||
elif field == 'authors':
|
||||
val = [v.replace('|', ',') for v in val]
|
||||
val = dfm['is_multiple'].join(val)
|
||||
else:
|
||||
val = apply_pattern(val)
|
||||
val = self.s_r_replace_mode_separator().join(val)
|
||||
|
||||
if fm['is_custom']:
|
||||
extra = self.db.get_custom_extra(id, label=fm['label'], index_is_id=True)
|
||||
self.db.set_custom(id, val, label=fm['label'], extra=extra,
|
||||
if dfm['is_custom']:
|
||||
extra = self.db.get_custom_extra(id, label=dfm['label'], index_is_id=True)
|
||||
self.db.set_custom(id, val, label=dfm['label'], extra=extra,
|
||||
commit=False)
|
||||
else:
|
||||
if field == 'comments':
|
||||
if dest == 'comments':
|
||||
setter = self.db.set_comment
|
||||
else:
|
||||
setter = getattr(self.db, 'set_'+field)
|
||||
setter = getattr(self.db, 'set_'+dest)
|
||||
setter(id, val, notify=False, commit=False)
|
||||
self.db.commit()
|
||||
dynamic['s_r_search_mode'] = self.search_mode.currentIndex()
|
||||
|
||||
def create_custom_column_editors(self):
|
||||
w = self.central_widget.widget(1)
|
||||
|
@ -319,7 +319,7 @@ Future conversion of these books will use the default settings.</string>
|
||||
<attribute name="title">
|
||||
<string>&Search and replace (experimental)</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<layout class="QGridLayout" name="testgrid">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetMinimumSize</enum>
|
||||
</property>
|
||||
@ -351,6 +351,39 @@ Future conversion of these books will use the default settings.</string>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="search_field"/>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<layout class="QHBoxLayout" name="HLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="xlabel_24">
|
||||
<property name="text">
|
||||
<string>Search mode:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>search_field</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="search_mode"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="HSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="xlabel_2">
|
||||
<property name="text">
|
||||
<string>&Search for:</string>
|
||||
@ -360,7 +393,20 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<item row="4" column="1">
|
||||
<widget class="HistoryLineEdit" name="search_for"/>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<widget class="QCheckBox" name="case_sensitive">
|
||||
<property name="text" >
|
||||
<string>Case sensitive</string>
|
||||
</property>
|
||||
<property name="checked" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="xlabel_4">
|
||||
<property name="text">
|
||||
<string>&Replace with:</string>
|
||||
@ -370,29 +416,93 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QComboBox" name="search_field"/>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="HistoryLineEdit" name="search_for"/>
|
||||
</item>
|
||||
<item row="4" column="2">
|
||||
<item row="5" column="1">
|
||||
<widget class="HistoryLineEdit" name="replace_with"/>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="label_41">
|
||||
<item row="5" column="2">
|
||||
<layout class="QHBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_41">
|
||||
<property name="text">
|
||||
<string>Apply function after replace:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>replace_func</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="replace_func"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="HSpacer_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="destination_field_label">
|
||||
<property name="text">
|
||||
<string>Apply function &after replace:</string>
|
||||
<string>&Destination field:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>replace_func</cstring>
|
||||
<cstring>destination_field</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<widget class="QComboBox" name="replace_func"/>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QComboBox" name="destination_field"/>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<layout class="QHBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="replace_mode_label">
|
||||
<property name="text">
|
||||
<string>Mode:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>replace_mode</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="replace_mode"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="comma_separated">
|
||||
<property name="text" >
|
||||
<string>use comma</string>
|
||||
</property>
|
||||
<property name="checked" >
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="zHSpacer_1">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLabel" name="xlabel_3">
|
||||
<property name="text">
|
||||
<string>Test &text</string>
|
||||
@ -402,8 +512,8 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="2">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<item row="7" column="2">
|
||||
<widget class="QLabel" name="label_51">
|
||||
<property name="text">
|
||||
<string>Test re&sult</string>
|
||||
</property>
|
||||
@ -412,17 +522,17 @@ Future conversion of these books will use the default settings.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_31">
|
||||
<property name="text">
|
||||
<string>Your test:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="8" column="1">
|
||||
<widget class="HistoryLineEdit" name="test_text"/>
|
||||
</item>
|
||||
<item row="7" column="2">
|
||||
<item row="8" column="2">
|
||||
<widget class="QLineEdit" name="test_result"/>
|
||||
</item>
|
||||
<item row="20" column="1">
|
||||
|
@ -464,11 +464,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
# change case don't cause any changes to the directories in the file
|
||||
# system. This can lead to having the directory names not match the
|
||||
# title/author, which leads to trouble when libraries are copied to
|
||||
# a case-sensitive system. The following code fixes this by checking
|
||||
# each segment. If they are different because of case, then rename
|
||||
# the segment to some temp file name, then rename it back to the
|
||||
# correct name. Note that the code above correctly handles files in
|
||||
# the directories, so no need to do them here.
|
||||
# a case-sensitive system. The following code attempts to fix this
|
||||
# by checking each segment. If they are different because of case,
|
||||
# then rename the segment to some temp file name, then rename it
|
||||
# back to the correct name. Note that the code above correctly
|
||||
# handles files in the directories, so no need to do them here.
|
||||
for oldseg, newseg in zip(c1, c2):
|
||||
if oldseg.lower() == newseg.lower() and oldseg != newseg:
|
||||
while True:
|
||||
@ -476,8 +476,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
tempname = os.path.join(curpath, 'TEMP.%f'%time.time())
|
||||
if not os.path.exists(tempname):
|
||||
break
|
||||
os.rename(os.path.join(curpath, oldseg), tempname)
|
||||
os.rename(tempname, os.path.join(curpath, newseg))
|
||||
try:
|
||||
os.rename(os.path.join(curpath, oldseg), tempname)
|
||||
except (IOError, OSError):
|
||||
# Windows (at least) sometimes refuses to do the rename
|
||||
# probably because a file such a cover is open in the
|
||||
# hierarchy. Just go on -- nothing is hurt beyond the
|
||||
# case of the filesystem not matching the case in
|
||||
# name stored by calibre
|
||||
print 'rename of library component failed'
|
||||
else:
|
||||
os.rename(tempname, os.path.join(curpath, newseg))
|
||||
curpath = os.path.join(curpath, newseg)
|
||||
|
||||
def add_listener(self, listener):
|
||||
@ -528,10 +537,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
|
||||
def get_field(self, idx, key, default=None, index_is_id=False):
|
||||
mi = self.get_metadata(idx, index_is_id=index_is_id, get_cover=True)
|
||||
try:
|
||||
return mi[key]
|
||||
except:
|
||||
return default
|
||||
return mi.get(key, default)
|
||||
|
||||
def standard_field_keys(self):
|
||||
return self.field_metadata.standard_field_keys()
|
||||
|
16
src/calibre/utils/pyconsole/__init__.py
Normal file
16
src/calibre/utils/pyconsole/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
|
||||
from calibre import prints as prints_
|
||||
|
||||
def prints(*args, **kwargs):
|
||||
kwargs['file'] = sys.__stdout__
|
||||
prints_(*args, **kwargs)
|
||||
|
||||
|
226
src/calibre/utils/pyconsole/editor.py
Normal file
226
src/calibre/utils/pyconsole/editor.py
Normal file
@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys, textwrap
|
||||
|
||||
from PyQt4.Qt import QTextEdit, Qt, QTextFrameFormat
|
||||
|
||||
from pygments.lexers import PythonLexer, PythonTracebackLexer
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.utils.pyconsole.formatter import Formatter
|
||||
from calibre.utils.pyconsole.repl import Interpreter, DummyFile
|
||||
from calibre.utils.pyconsole import prints
|
||||
|
||||
class EditBlock(object): # {{{
|
||||
|
||||
def __init__(self, cursor):
|
||||
self.cursor = cursor
|
||||
|
||||
def __enter__(self):
|
||||
self.cursor.beginEditBlock()
|
||||
return self.cursor
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.cursor.endEditBlock()
|
||||
# }}}
|
||||
|
||||
class Editor(QTextEdit):
|
||||
|
||||
@property
|
||||
def doc(self):
|
||||
return self.document()
|
||||
|
||||
@property
|
||||
def cursor(self):
|
||||
return self.textCursor()
|
||||
|
||||
@property
|
||||
def root_frame(self):
|
||||
return self.doc.rootFrame()
|
||||
|
||||
@property
|
||||
def cursor_pos(self):
|
||||
pass
|
||||
#pos = self.cursor.position() - self.prompt_frame.firstPosition()
|
||||
#i = 0
|
||||
#for line in self.current_prompt:
|
||||
# i += self.prompt_len
|
||||
|
||||
def __init__(self,
|
||||
prompt='>>> ',
|
||||
continuation='... ',
|
||||
parent=None):
|
||||
QTextEdit.__init__(self, parent)
|
||||
self.buf = ''
|
||||
self.prompt_frame = None
|
||||
self.current_prompt = ['']
|
||||
self.allow_output = False
|
||||
self.prompt_frame_format = QTextFrameFormat()
|
||||
self.prompt_frame_format.setBorder(1)
|
||||
self.prompt_frame_format.setBorderStyle(QTextFrameFormat.BorderStyle_Solid)
|
||||
self.prompt_len = len(prompt)
|
||||
|
||||
self.doc.setMaximumBlockCount(10000)
|
||||
self.lexer = PythonLexer(ensurenl=False)
|
||||
self.tb_lexer = PythonTracebackLexer()
|
||||
self.formatter = Formatter(prompt, continuation)
|
||||
|
||||
motd = textwrap.dedent('''\
|
||||
# Python {0}
|
||||
# {1} {2}
|
||||
'''.format(sys.version.splitlines()[0], __appname__,
|
||||
__version__))
|
||||
|
||||
with EditBlock(self.cursor):
|
||||
self.render_block(motd)
|
||||
|
||||
sys.stdout = sys.stderr = DummyFile(parent=self)
|
||||
sys.stdout.write_output.connect(self.show_output)
|
||||
self.interpreter = Interpreter(parent=self)
|
||||
self.interpreter.show_error.connect(self.show_error)
|
||||
|
||||
#it = self.prompt_frame.begin()
|
||||
#while not it.atEnd():
|
||||
# bl = it.currentBlock()
|
||||
# prints(repr(bl.text()))
|
||||
# it += 1
|
||||
|
||||
|
||||
# Rendering {{{
|
||||
|
||||
def render_block(self, text, restore_prompt=True):
|
||||
self.formatter.render(self.lexer.get_tokens(text), self.cursor)
|
||||
self.cursor.insertBlock()
|
||||
self.cursor.movePosition(self.cursor.End)
|
||||
if restore_prompt:
|
||||
self.render_current_prompt()
|
||||
|
||||
def clear_current_prompt(self):
|
||||
if self.prompt_frame is None:
|
||||
c = self.root_frame.lastCursorPosition()
|
||||
self.prompt_frame = c.insertFrame(self.prompt_frame_format)
|
||||
self.setTextCursor(c)
|
||||
else:
|
||||
c = self.prompt_frame.firstCursorPosition()
|
||||
self.setTextCursor(c)
|
||||
c.setPosition(self.prompt_frame.lastPosition(), c.KeepAnchor)
|
||||
c.removeSelectedText()
|
||||
c.setPosition(self.prompt_frame.firstPosition())
|
||||
|
||||
def render_current_prompt(self):
|
||||
self.clear_current_prompt()
|
||||
|
||||
for i, line in enumerate(self.current_prompt):
|
||||
start = i == 0
|
||||
end = i == len(self.current_prompt) - 1
|
||||
self.formatter.render_prompt(not start, self.cursor)
|
||||
self.formatter.render(self.lexer.get_tokens(line), self.cursor)
|
||||
if not end:
|
||||
self.cursor.insertText('\n')
|
||||
|
||||
def show_error(self, is_syntax_err, tb):
|
||||
if self.prompt_frame is not None:
|
||||
# At a prompt, so redirect output
|
||||
return prints(tb)
|
||||
try:
|
||||
self.buf += tb
|
||||
if is_syntax_err:
|
||||
self.formatter.render_syntax_error(tb, self.cursor)
|
||||
else:
|
||||
self.formatter.render(self.tb_lexer.get_tokens(tb), self.cursor)
|
||||
except:
|
||||
prints(tb)
|
||||
|
||||
def show_output(self, raw):
|
||||
if self.prompt_frame is not None:
|
||||
# At a prompt, so redirect output
|
||||
return prints(raw)
|
||||
try:
|
||||
self.current_prompt_range = None
|
||||
self.buf += raw
|
||||
self.formatter.render_raw(raw, self.cursor)
|
||||
except:
|
||||
prints(raw)
|
||||
|
||||
# }}}
|
||||
|
||||
# Keyboard handling {{{
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
text = unicode(ev.text())
|
||||
key = ev.key()
|
||||
if key in (Qt.Key_Enter, Qt.Key_Return):
|
||||
self.enter_pressed()
|
||||
elif key == Qt.Key_Home:
|
||||
self.home_pressed()
|
||||
elif key == Qt.Key_End:
|
||||
self.end_pressed()
|
||||
elif key == Qt.Key_Left:
|
||||
self.left_pressed()
|
||||
elif key == Qt.Key_Right:
|
||||
self.right_pressed()
|
||||
elif text:
|
||||
self.text_typed(text)
|
||||
else:
|
||||
QTextEdit.keyPressEvent(self, ev)
|
||||
|
||||
def left_pressed(self):
|
||||
pass
|
||||
|
||||
def right_pressed(self):
|
||||
if self.prompt_frame is not None:
|
||||
c = self.cursor
|
||||
c.movePosition(c.NextCharacter)
|
||||
self.setTextCursor(c)
|
||||
|
||||
def home_pressed(self):
|
||||
if self.prompt_frame is not None:
|
||||
c = self.cursor
|
||||
c.movePosition(c.StartOfLine)
|
||||
c.movePosition(c.NextCharacter, n=self.prompt_len)
|
||||
self.setTextCursor(c)
|
||||
|
||||
def end_pressed(self):
|
||||
if self.prompt_frame is not None:
|
||||
c = self.cursor
|
||||
c.movePosition(c.EndOfLine)
|
||||
self.setTextCursor(c)
|
||||
|
||||
def enter_pressed(self):
|
||||
if self.prompt_frame is None:
|
||||
return
|
||||
if self.current_prompt[0]:
|
||||
c = self.root_frame.lastCursorPosition()
|
||||
self.setTextCursor(c)
|
||||
old_pf = self.prompt_frame
|
||||
self.prompt_frame = None
|
||||
oldbuf = self.buf
|
||||
self.buf = ''
|
||||
ret = self.interpreter.runsource('\n'.join(self.current_prompt))
|
||||
if ret: # Incomplete command
|
||||
self.buf = oldbuf
|
||||
self.prompt_frame = old_pf
|
||||
self.current_prompt.append('')
|
||||
else: # Command completed
|
||||
self.current_prompt = ['']
|
||||
old_pf.setFrameFormat(QTextFrameFormat())
|
||||
self.render_current_prompt()
|
||||
|
||||
def text_typed(self, text):
|
||||
if not self.current_prompt[0]:
|
||||
self.cursor.beginEditBlock()
|
||||
else:
|
||||
self.cursor.joinPreviousEditBlock()
|
||||
self.current_prompt[-1] += text
|
||||
self.render_current_prompt()
|
||||
self.cursor.endEditBlock()
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
|
80
src/calibre/utils/pyconsole/formatter.py
Normal file
80
src/calibre/utils/pyconsole/formatter.py
Normal file
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from PyQt4.Qt import QTextCharFormat, QFont, QBrush, QColor
|
||||
|
||||
from pygments.formatter import Formatter as PF
|
||||
from pygments.token import Token
|
||||
|
||||
class Formatter(object):
|
||||
|
||||
def __init__(self, prompt, continuation, **options):
|
||||
if len(prompt) != len(continuation):
|
||||
raise ValueError('%r does not have the same length as %r' %
|
||||
(prompt, continuation))
|
||||
|
||||
self.prompt, self.continuation = prompt, continuation
|
||||
|
||||
pf = PF(**options)
|
||||
self.styles = {}
|
||||
self.normal = self.base_fmt()
|
||||
for ttype, ndef in pf.style:
|
||||
fmt = self.base_fmt()
|
||||
if ndef['color']:
|
||||
fmt.setForeground(QBrush(QColor('#%s'%ndef['color'])))
|
||||
fmt.setUnderlineColor(QColor('#%s'%ndef['color']))
|
||||
if ndef['bold']:
|
||||
fmt.setFontWeight(QFont.Bold)
|
||||
if ndef['italic']:
|
||||
fmt.setFontItalic(True)
|
||||
if ndef['underline']:
|
||||
fmt.setFontUnderline(True)
|
||||
if ndef['bgcolor']:
|
||||
fmt.setBackground(QBrush(QColor('#%s'%ndef['bgcolor'])))
|
||||
if ndef['border']:
|
||||
pass # No support for borders
|
||||
|
||||
self.styles[ttype] = fmt
|
||||
|
||||
def base_fmt(self):
|
||||
fmt = QTextCharFormat()
|
||||
fmt.setFontFamily('monospace')
|
||||
return fmt
|
||||
|
||||
def render_raw(self, raw, cursor):
|
||||
cursor.insertText(raw, self.normal)
|
||||
|
||||
def render_syntax_error(self, tb, cursor):
|
||||
fmt = self.styles[Token.Error]
|
||||
cursor.insertText(tb, fmt)
|
||||
|
||||
def render(self, tokens, cursor):
|
||||
lastval = ''
|
||||
lasttype = None
|
||||
|
||||
for ttype, value in tokens:
|
||||
while ttype not in self.styles:
|
||||
ttype = ttype.parent
|
||||
if ttype == lasttype:
|
||||
lastval += value
|
||||
else:
|
||||
if lastval:
|
||||
fmt = self.styles[lasttype]
|
||||
cursor.insertText(lastval, fmt)
|
||||
lastval = value
|
||||
lasttype = ttype
|
||||
|
||||
if lastval:
|
||||
fmt = self.styles[lasttype]
|
||||
cursor.insertText(lastval, fmt)
|
||||
|
||||
def render_prompt(self, is_continuation, cursor):
|
||||
pr = self.continuation if is_continuation else self.prompt
|
||||
fmt = self.styles[Token.Generic.Subheading]
|
||||
cursor.insertText(pr, fmt)
|
||||
|
||||
|
56
src/calibre/utils/pyconsole/main.py
Normal file
56
src/calibre/utils/pyconsole/main.py
Normal file
@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__version__ = '0.1.0'
|
||||
|
||||
from PyQt4.Qt import QMainWindow, QToolBar, QStatusBar, QLabel, QFont, Qt, \
|
||||
QApplication
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.utils.pyconsole.editor import Editor
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self, default_status_msg):
|
||||
|
||||
QMainWindow.__init__(self)
|
||||
|
||||
self.resize(600, 700)
|
||||
|
||||
# Setup status bar {{{
|
||||
self.status_bar = QStatusBar(self)
|
||||
self.status_bar.defmsg = QLabel(__appname__ + _(' console ') +
|
||||
__version__)
|
||||
self.status_bar._font = QFont()
|
||||
self.status_bar._font.setBold(True)
|
||||
self.status_bar.defmsg.setFont(self.status_bar._font)
|
||||
self.status_bar.addWidget(self.status_bar.defmsg)
|
||||
self.setStatusBar(self.status_bar)
|
||||
# }}}
|
||||
|
||||
# Setup tool bar {{{
|
||||
self.tool_bar = QToolBar(self)
|
||||
self.addToolBar(Qt.BottomToolBarArea, self.tool_bar)
|
||||
self.tool_bar.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||
# }}}
|
||||
|
||||
self.editor = Editor(parent=self)
|
||||
self.setCentralWidget(self.editor)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
QApplication.setApplicationName(__appname__+' console')
|
||||
QApplication.setOrganizationName('Kovid Goyal')
|
||||
app = QApplication([])
|
||||
m = MainWindow(_('Welcome to') + ' ' + __appname__+' console')
|
||||
m.show()
|
||||
app.exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
67
src/calibre/utils/pyconsole/repl.py
Normal file
67
src/calibre/utils/pyconsole/repl.py
Normal file
@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from code import InteractiveInterpreter
|
||||
|
||||
from PyQt4.Qt import QObject, pyqtSignal
|
||||
|
||||
from calibre import isbytestring
|
||||
from calibre.constants import preferred_encoding
|
||||
|
||||
class Interpreter(QObject, InteractiveInterpreter):
|
||||
|
||||
# show_error(is_syntax_error, traceback)
|
||||
show_error = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, local={}, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
if '__name__' not in local:
|
||||
local['__name__'] = '__console__'
|
||||
if '__doc__' not in local:
|
||||
local['__doc__'] = None
|
||||
InteractiveInterpreter.__init__(self, locals=local)
|
||||
|
||||
def showtraceback(self, *args, **kwargs):
|
||||
self.is_syntax_error = False
|
||||
InteractiveInterpreter.showtraceback(self, *args, **kwargs)
|
||||
|
||||
def showsyntaxerror(self, *args, **kwargs):
|
||||
self.is_syntax_error = True
|
||||
InteractiveInterpreter.showsyntaxerror(self, *args, **kwargs)
|
||||
|
||||
def write(self, tb):
|
||||
self.show_error.emit(self.is_syntax_error, tb)
|
||||
|
||||
class DummyFile(QObject):
|
||||
|
||||
# write_output(unicode_object)
|
||||
write_output = pyqtSignal(object)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QObject.__init__(self, parent)
|
||||
self.closed = False
|
||||
self.name = 'console'
|
||||
self.softspace = 0
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def write(self, raw):
|
||||
#import sys, traceback
|
||||
#print >> sys.__stdout__, 'file,write stack:\n', ''.join(traceback.format_stack())
|
||||
if isbytestring(raw):
|
||||
try:
|
||||
raw = raw.decode(preferred_encoding, 'replace')
|
||||
except:
|
||||
raw = repr(raw)
|
||||
if isbytestring(raw):
|
||||
raw = raw.decode(preferred_encoding, 'replace')
|
||||
self.write_output.emit(raw)
|
||||
|
Loading…
x
Reference in New Issue
Block a user