Date: Mon, 13 Dec 2010 17:07:00 -0700
Subject: [PATCH 3/6] Infrastructure to detect mem leaks in the edit metadata
dialog
---
src/calibre/gui2/actions/edit_metadata.py | 8 ++-
src/calibre/gui2/dialogs/metadata_single.py | 69 ++++++++++++++++++++-
src/calibre/utils/mem.py | 55 ++++++++++++++++
3 files changed, 126 insertions(+), 6 deletions(-)
create mode 100644 src/calibre/utils/mem.py
diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index 60a943ccb9..11949632e9 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -154,15 +154,17 @@ class EditMetadataAction(InterfaceAction):
d.view_format.connect(lambda
fmt:self.gui.iactions['View'].view_format(row_list[current_row],
fmt))
- if d.exec_() != d.Accepted:
- d.view_format.disconnect()
+ ret = d.exec_()
+ d.break_cycles()
+ if ret != d.Accepted:
break
- d.view_format.disconnect()
+
changed.add(d.id)
if d.row_delta == 0:
break
current_row += d.row_delta
+
if changed:
self.gui.library_view.model().refresh_ids(list(changed))
current = self.gui.library_view.currentIndex()
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index d9bb1c2a33..9e3a8c7eda 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -293,7 +293,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
- self.pi.stop()
+ if self.pi is not None:
+ self.pi.stop()
# }}}
@@ -442,7 +443,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
ResizableDialog.__init__(self, window)
self.cover_fetcher = None
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
- self.cancel_all = False
base = unicode(self.author_sort.toolTip())
self.ok_aus_tooltip = '' + textwrap.fill(base+'
'+
_(' The green color indicates that the current '
@@ -573,7 +573,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
self.series.lineEdit().editingFinished.connect(self.increment_series_index)
- self.show()
pm = QPixmap()
if cover:
pm.loadFromData(cover)
@@ -593,6 +592,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip()
+ self.show()
+
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
layout = w.layout()
@@ -907,3 +908,65 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
dynamic.set('metasingle_window_geometry', bytes(self.saveGeometry()))
dynamic.set('metasingle_splitter_state',
bytes(self.splitter.saveState()))
+
+ def break_cycles(self):
+ try:
+ self.view_format.disconnect()
+ except:
+ pass # Fails if view format was never connected
+ self.view_format = None
+ self.db = None
+ self.pi = None
+ self.cover_data = self.cpixmap = None
+
+if __name__ == '__main__':
+ from calibre.library import db
+ from PyQt4.Qt import QApplication
+ from calibre.utils.mem import memory
+ import gc
+
+
+ app = QApplication([])
+ db = db()
+
+ # Initialize all Qt Objects once
+ d = MetadataSingleDialog(None, 4, db)
+ d.break_cycles()
+ d.reject()
+ del d
+
+ for i in range(3):
+ gc.collect()
+ before = memory()
+
+ gc.collect()
+ d = MetadataSingleDialog(None, 4, db)
+ d.break_cycles()
+ d.reject()
+ del d
+
+ for i in range(3):
+ gc.collect()
+ print 'Used memory:', memory(before)/1024.**2, 'MB'
+ gc.collect()
+
+ '''
+ nmap, omap = {}, {}
+ for x in objects:
+ omap[id(x)] = x
+ for x in nobjects:
+ nmap[id(x)] = x
+
+ new_ids = set(nmap.keys()) - set(omap.keys())
+ print "New ids:", len(new_ids)
+ for i in new_ids:
+ o = nmap[i]
+ if o is objects:
+ continue
+ print repr(o)[:1050]
+ refs = gc.get_referrers(o)
+ for r in refs:
+ if r is objects or r is nobjects:
+ continue
+ print '\t', r
+ '''
diff --git a/src/calibre/utils/mem.py b/src/calibre/utils/mem.py
new file mode 100644
index 0000000000..f48aec34c6
--- /dev/null
+++ b/src/calibre/utils/mem.py
@@ -0,0 +1,55 @@
+#!/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'
+
+## {{{ http://code.activestate.com/recipes/286222/ (r1)
+import os
+
+_proc_status = '/proc/%d/status' % os.getpid()
+
+_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0,
+ 'KB': 1024.0, 'MB': 1024.0*1024.0}
+
+def _VmB(VmKey):
+ '''Private.
+ '''
+ global _proc_status, _scale
+ # get pseudo file /proc//status
+ try:
+ t = open(_proc_status)
+ v = t.read()
+ t.close()
+ except:
+ return 0.0 # non-Linux?
+ # get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
+ i = v.index(VmKey)
+ v = v[i:].split(None, 3) # whitespace
+ if len(v) < 3:
+ return 0.0 # invalid format?
+ # convert Vm value to bytes
+ return float(v[1]) * _scale[v[2]]
+
+
+def memory(since=0.0):
+ '''Return memory usage in bytes.
+ '''
+ return _VmB('VmSize:') - since
+
+
+def resident(since=0.0):
+ '''Return resident memory usage in bytes.
+ '''
+ return _VmB('VmRSS:') - since
+
+
+def stacksize(since=0.0):
+ '''Return stack size in bytes.
+ '''
+ return _VmB('VmStk:') - since
+## end of http://code.activestate.com/recipes/286222/ }}}
+
+
+
From f71f60ab0cb84364a876dc7ee3950474e115d338 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 13 Dec 2010 19:02:14 -0700
Subject: [PATCH 4/6] Edit metadata dialog: Fix memory leak caused by
Next/Previous buttons
---
src/calibre/gui2/dialogs/metadata_single.py | 47 +++++++--------------
1 file changed, 15 insertions(+), 32 deletions(-)
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
index 9e3a8c7eda..4a9bb784c8 100644
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ b/src/calibre/gui2/dialogs/metadata_single.py
@@ -910,14 +910,18 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
bytes(self.splitter.saveState()))
def break_cycles(self):
- try:
- self.view_format.disconnect()
- except:
- pass # Fails if view format was never connected
- self.view_format = None
- self.db = None
- self.pi = None
- self.cover_data = self.cpixmap = None
+ # Break any reference cycles that could prevent python
+ # from garbage collecting this dialog
+ def disconnect(signal):
+ try:
+ signal.disconnect()
+ except:
+ pass # Fails if view format was never connected
+ disconnect(self.view_format)
+ for b in ('next_button', 'prev_button'):
+ x = getattr(self, b, None)
+ if x is not None:
+ disconnect(x.clicked)
if __name__ == '__main__':
from calibre.library import db
@@ -935,38 +939,17 @@ if __name__ == '__main__':
d.reject()
del d
- for i in range(3):
+ for i in range(5):
gc.collect()
before = memory()
- gc.collect()
d = MetadataSingleDialog(None, 4, db)
- d.break_cycles()
d.reject()
+ d.break_cycles()
del d
- for i in range(3):
+ for i in range(5):
gc.collect()
print 'Used memory:', memory(before)/1024.**2, 'MB'
- gc.collect()
- '''
- nmap, omap = {}, {}
- for x in objects:
- omap[id(x)] = x
- for x in nobjects:
- nmap[id(x)] = x
- new_ids = set(nmap.keys()) - set(omap.keys())
- print "New ids:", len(new_ids)
- for i in new_ids:
- o = nmap[i]
- if o is objects:
- continue
- print repr(o)[:1050]
- refs = gc.get_referrers(o)
- for r in refs:
- if r is objects or r is nobjects:
- continue
- print '\t', r
- '''
From c11bbe3eea7c6003787b66b44e19a280686228ee Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 13 Dec 2010 22:06:37 -0700
Subject: [PATCH 5/6] Fix string.letters in non english locales
---
src/calibre/library/__init__.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/library/__init__.py b/src/calibre/library/__init__.py
index 8ff23c0a0a..177c5063ac 100644
--- a/src/calibre/library/__init__.py
+++ b/src/calibre/library/__init__.py
@@ -19,12 +19,15 @@ def generate_test_db(library_path,
max_tags=10
):
import random, string, os, sys, time
+ from calibre.constants import preferred_encoding
if not os.path.exists(library_path):
os.makedirs(library_path)
+ letters = string.letters.decode(preferred_encoding)
+
def randstr(length):
- return ''.join(random.choice(string.letters) for i in
+ return ''.join(random.choice(letters) for i in
xrange(length))
all_tags = [randstr(tag_length) for j in xrange(num_of_tags)]
From d6f5f634eacced09f7208613df58952f1bfe95e9 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 13 Dec 2010 23:17:13 -0700
Subject: [PATCH 6/6] Allow changing the font used in the calibre interface via
Preferences->Look and feel
---
src/calibre/gui2/__init__.py | 7 ++-
src/calibre/gui2/preferences/look_feel.py | 59 +++++++++++++++++++++--
src/calibre/gui2/preferences/look_feel.ui | 16 +++++-
3 files changed, 77 insertions(+), 5 deletions(-)
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 57ca2a1880..f96c64080d 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -9,7 +9,7 @@ from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
- QIcon, QApplication, QDialog, QPushButton, QUrl
+ QIcon, QApplication, QDialog, QPushButton, QUrl, QFont
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
@@ -52,6 +52,7 @@ gprefs.defaults['show_splash_screen'] = True
gprefs.defaults['toolbar_icon_size'] = 'medium'
gprefs.defaults['toolbar_text'] = 'auto'
gprefs.defaults['show_child_bar'] = False
+gprefs.defaults['font'] = None
# }}}
@@ -613,6 +614,10 @@ class Application(QApplication):
qt_app = self
self._file_open_paths = []
self._file_open_lock = RLock()
+ self.original_font = QFont(QApplication.font())
+ fi = gprefs['font']
+ if fi is not None:
+ QApplication.setFont(QFont(*fi))
def _send_file_open_events(self):
with self._file_open_lock:
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 10c2fcfe95..b2ba87d1e0 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -5,10 +5,11 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
+from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.look_feel_ui import Ui_Form
-from calibre.gui2 import config, gprefs
+from calibre.gui2 import config, gprefs, qt_app
from calibre.utils.localization import available_translations, \
get_language, get_lang
from calibre.utils.config import prefs
@@ -56,12 +57,64 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
(_('Never'), 'never')]
r('toolbar_text', gprefs, choices=choices)
+ self.current_font = None
+ self.change_font_button.clicked.connect(self.change_font)
+
+
+ def initialize(self):
+ ConfigWidgetBase.initialize(self)
+ self.current_font = gprefs['font']
+ self.update_font_display()
+
+ def restore_defaults(self):
+ ConfigWidgetBase.restore_defaults(self)
+ ofont = self.current_font
+ self.current_font = None
+ if ofont is not None:
+ self.changed_signal.emit()
+ self.update_font_display()
+
+ def build_font_obj(self):
+ font_info = self.current_font
+ if font_info is not None:
+ font = QFont(*font_info)
+ else:
+ font = qt_app.original_font
+ return font
+
+ def update_font_display(self):
+ font = self.build_font_obj()
+ fi = QFontInfo(font)
+ name = unicode(fi.family())
+
+ self.font_display.setFont(font)
+ self.font_display.setText(_('Current font:') + ' ' + name +
+ ' [%dpt]'%fi.pointSize())
+
+ def change_font(self, *args):
+ fd = QFontDialog(self.build_font_obj(), self)
+ if fd.exec_() == fd.Accepted:
+ font = fd.selectedFont()
+ fi = QFontInfo(font)
+ self.current_font = (unicode(fi.family()), fi.pointSize(),
+ fi.weight(), fi.italic())
+ self.update_font_display()
+ self.changed_signal.emit()
+
+ def commit(self, *args):
+ rr = ConfigWidgetBase.commit(self, *args)
+ if self.current_font != gprefs['font']:
+ gprefs['font'] = self.current_font
+ QApplication.setFont(self.font_display.font())
+ rr = True
+ return rr
+
+
def refresh_gui(self, gui):
gui.search.search_as_you_type(config['search_as_you_type'])
-
+ self.update_font_display()
if __name__ == '__main__':
- from PyQt4.Qt import QApplication
app = QApplication([])
test_widget('Interface', 'Look & Feel')
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index 1de55d51ef..91f45a155f 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -183,7 +183,7 @@
- -
+
-
Qt::Vertical
@@ -196,6 +196,20 @@
+ -
+
+
+ true
+
+
+
+ -
+
+
+ Change &font (needs restart)
+
+
+