E-book viewer: Add an option to allow only a single book to be viewed at a time. Trying to view a second book will cause it to replace the currently viewed book. Fixes #1526504 [view: reuse current instance option](https://bugs.launchpad.net/calibre/+bug/1526504)

This commit is contained in:
Kovid Goyal 2016-01-14 17:40:24 +05:30
parent 8c8ae49884
commit ca0854ce2e
4 changed files with 165 additions and 48 deletions

View File

@ -291,8 +291,9 @@ class ConfigDialog(QDialog, Ui_Dialog):
def restore_defaults(self):
opts = config('').parse()
self.load_options(opts)
from calibre.gui2.viewer.main import dprefs
from calibre.gui2.viewer.main import dprefs, vprefs
self.word_lookups = dprefs.defaults['word_lookups']
self.opt_singleinstance.setChecked(vprefs.defaults['singleinstance'])
def load_options(self, opts):
self.opt_remember_window_size.setChecked(opts.remember_window_size)
@ -344,6 +345,8 @@ class ConfigDialog(QDialog, Ui_Dialog):
setattr(self, 'current_%s_color'%x, getattr(opts, '%s_color'%x))
self.update_sample_colors()
self.opt_show_controls.setChecked(opts.show_controls)
from calibre.gui2.viewer.main import vprefs
self.opt_singleinstance.setChecked(bool(vprefs['singleinstance']))
def change_color(self, which, reset=False):
if reset:
@ -429,5 +432,6 @@ class ConfigDialog(QDialog, Ui_Dialog):
c.set('show_controls', self.opt_show_controls.isChecked())
for x in ('top', 'bottom', 'side'):
c.set(x+'_margin', int(getattr(self, 'opt_%s_margin'%x).value()))
from calibre.gui2.viewer.main import dprefs
from calibre.gui2.viewer.main import dprefs, vprefs
dprefs['word_lookups'] = self.word_lookups
vprefs['singleinstance'] = self.opt_singleinstance.isChecked()

View File

@ -68,7 +68,7 @@ QToolBox::tab:hover {
<x>0</x>
<y>0</y>
<width>799</width>
<height>378</height>
<height>370</height>
</rect>
</property>
<attribute name="label">
@ -241,7 +241,7 @@ QToolBox::tab:hover {
<x>0</x>
<y>0</y>
<width>799</width>
<height>378</height>
<height>370</height>
</rect>
</property>
<attribute name="label">
@ -414,8 +414,8 @@ QToolBox::tab:hover {
<rect>
<x>0</x>
<y>0</y>
<width>381</width>
<height>193</height>
<width>799</width>
<height>370</height>
</rect>
</property>
<attribute name="label">
@ -516,8 +516,8 @@ QToolBox::tab:hover {
<rect>
<x>0</x>
<y>0</y>
<width>340</width>
<height>70</height>
<width>799</width>
<height>370</height>
</rect>
</property>
<attribute name="label">
@ -595,8 +595,8 @@ QToolBox::tab:hover {
<rect>
<x>0</x>
<y>0</y>
<width>384</width>
<height>140</height>
<width>799</width>
<height>370</height>
</rect>
</property>
<attribute name="label">
@ -673,8 +673,8 @@ QToolBox::tab:hover {
<rect>
<x>0</x>
<y>0</y>
<width>479</width>
<height>226</height>
<width>799</width>
<height>370</height>
</rect>
</property>
<attribute name="label">
@ -688,20 +688,36 @@ QToolBox::tab:hover {
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_current_page">
<property name="text">
<string>Remember the &amp;current page when quitting</string>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_copy_bookmarks_to_file">
<property name="toolTip">
<string>Keep a copy of all bookmarks/current page information inside the ebook file, so that you can share them by simply sending the ebook file itself. Currently only works with ebooks in the EPUB format.</string>
</property>
<property name="text">
<string>Keep a copy of bookmarks/current page inside the ebook file, for easy sharing</string>
<string>Keep a copy of &amp;bookmarks/current page inside the ebook file, for easy sharing</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="search_online_url">
<property name="toolTip">
<string>Change the search engine used to perform online searches for selected text.
You must enter the search URL for the search engine, with the placeholder
{text}, which will be replaced by the selected text.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="hyphenate_default_lang">
<property name="toolTip">
<string>The default language to use for hyphenation rules. If the book does not specify a language, this will be used.</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remember_current_page">
<property name="text">
<string>Remember the &amp;current page when quitting</string>
</property>
</widget>
</item>
@ -719,7 +735,7 @@ QToolBox::tab:hover {
</property>
</widget>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QPushButton" name="clear_search_history_button">
<property name="text">
<string>Clear search history</string>
@ -736,13 +752,6 @@ QToolBox::tab:hover {
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="hyphenate_default_lang">
<property name="toolTip">
<string>The default language to use for hyphenation rules. If the book does not specify a language, this will be used.</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
@ -753,16 +762,7 @@ QToolBox::tab:hover {
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="search_online_url">
<property name="toolTip">
<string>Change the search engine used to perform online searches for selected text.
You must enter the search URL for the search engine, with the placeholder
{text}, which will be replaced by the selected text.</string>
</property>
</widget>
</item>
<item row="8" column="0">
<item row="9" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -775,6 +775,16 @@ You must enter the search URL for the search engine, with the placeholder
</property>
</spacer>
</item>
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="opt_singleinstance">
<property name="toolTip">
<string>Normally, you can view multiple books in calibre, each in its own viewer window. With this option, if you attempt to view a second book, it will replace the previously opened book instead of using a new window.</string>
</property>
<property name="text">
<string>Allow only a &amp;single book to be viewed at a time (needs restart)</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

View File

@ -8,7 +8,7 @@ from collections import namedtuple
from PyQt5.Qt import (
QApplication, Qt, QIcon, QTimer, QByteArray, QSize, QTime,
QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex)
QPropertyAnimation, QUrl, QInputDialog, QAction, QModelIndex, pyqtSignal)
from calibre.gui2.viewer.ui import Main as MainWindow
from calibre.gui2.viewer.toc import TOC
@ -20,14 +20,17 @@ from calibre.ebooks.oeb.iterator.book import EbookIterator
from calibre.constants import islinux, filesystem_encoding
from calibre.utils.config import Config, StringConfig, JSONConfig
from calibre.customize.ui import available_input_formats
from calibre import as_unicode, force_unicode, isbytestring
from calibre import as_unicode, force_unicode, isbytestring, prints
from calibre.ptempfile import reset_base_dir
from calibre.utils.ipc import viewer_socket_address, RC
from calibre.utils.zipfile import BadZipfile
from calibre.utils.localization import canonicalize_lang, lang_as_iso639_1, get_lang
vprefs = JSONConfig('viewer')
vprefs.defaults['singleinstance'] = False
dprefs = JSONConfig('viewer_dictionaries')
dprefs.defaults['word_lookups'] = {}
singleinstance_name = 'calibre_viewer'
class Worker(Thread):
@ -70,6 +73,19 @@ def lookup_website(lang):
wm = dprefs['word_lookups']
return wm.get(lang, default_lookup_website(lang))
def listen(self):
while True:
try:
conn = self.listener.accept()
except Exception:
break
try:
self.msg_from_anotherinstance.emit(conn.recv())
conn.close()
except Exception as e:
prints('Failed to read message from other instance with error: %s' % as_unicode(e))
self.listener = None
class EbookViewer(MainWindow):
STATE_VERSION = 2
@ -78,13 +94,20 @@ class EbookViewer(MainWindow):
PAGED_MODE_TT = _('Switch to flow mode - where the text is not broken up '
'into pages')
AUTOSAVE_INTERVAL = 10 # seconds
msg_from_anotherinstance = pyqtSignal(object)
def __init__(self, pathtoebook=None, debug_javascript=False, open_at=None,
start_in_fullscreen=False, continue_reading=False):
start_in_fullscreen=False, continue_reading=False, listener=None):
MainWindow.__init__(self, debug_javascript)
self.view.magnification_changed.connect(self.magnification_changed)
self.closed = False
self.show_toc_on_open = False
self.listener = listener
if listener is not None:
t = Thread(name='ConnListener', target=listen, args=(self,))
t.daemon = True
t.start()
self.msg_from_anotherinstance.connect(self.another_instance_wants_to_talk, type=Qt.QueuedConnection)
self.current_book_has_toc = False
self.iterator = None
self.current_page = None
@ -271,6 +294,8 @@ class EbookViewer(MainWindow):
self.action_full_screen.trigger()
return False
self.save_state()
if self.listener is not None:
self.listener.close()
return True
def quit(self):
@ -858,6 +883,14 @@ class EbookViewer(MainWindow):
except:
traceback.print_exc()
def another_instance_wants_to_talk(self, msg):
try:
path, open_at = msg
except Exception:
return
self.load_ebook(path, open_at=open_at)
self.raise_()
def load_ebook(self, pathtoebook, open_at=None, reopen_at=None):
if self.iterator is not None:
self.save_current_position()
@ -1080,6 +1113,40 @@ View an ebook.
setup_gui_option_parser(parser)
return parser
def create_listener():
if islinux:
from calibre.utils.ipc.server import LinuxListener as Listener
else:
from multiprocessing.connection import Listener
return Listener(address=viewer_socket_address())
def ensure_single_instance(args, open_at):
try:
from calibre.utils.lock import singleinstance
si = singleinstance(singleinstance_name)
except Exception:
import traceback
error_dialog(None, _('Cannot start viewer'), _(
'Failed to start viewer, single instance locking failed. Click "Show Details" for more information'),
det_msg=traceback.format_exc(), show=True)
raise SystemExit(1)
if not si:
if len(args) > 1:
t = RC(print_error=True, socket_address=viewer_socket_address())
t.start()
t.join(3.0)
if t.is_alive() or t.conn is None:
error_dialog(None, _('Connect talk to viewer'), _(
'Unable to connect to existing viewer window, try restarting it.'), show=True)
raise SystemExit(1)
t.conn.send((os.path.abspath(args[1]), open_at))
t.conn.close()
prints('Opened book in existing viewer instance')
raise SystemExit(0)
listener = create_listener()
return listener
def main(args=sys.argv):
# Ensure viewer can continue to function if GUI is closed
@ -1089,15 +1156,25 @@ def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
open_at = float(opts.open_at.replace(',', '.')) if opts.open_at else None
listener = None
override = 'calibre-ebook-viewer' if islinux else None
app = Application(args, override_program_name=override, color_prefs=vprefs)
app.load_builtin_fonts()
app.setWindowIcon(QIcon(I('viewer.png')))
QApplication.setOrganizationName(ORG_NAME)
QApplication.setApplicationName(APP_UID)
if vprefs['singleinstance']:
try:
listener = ensure_single_instance(args, open_at)
except Exception as e:
import traceback
error_dialog(None, _('Failed to start viewer'), as_unicode(e), det_msg=traceback.format_exc(), show=True)
raise SystemExit(1)
main = EbookViewer(args[1] if len(args) > 1 else None,
debug_javascript=opts.debug_javascript, open_at=open_at, continue_reading=opts.continue_reading,
start_in_fullscreen=opts.full_screen)
start_in_fullscreen=opts.full_screen, listener=listener)
app.installEventFilter(main)
# This is needed for paged mode. Without it, the first document that is
# loaded will have extra blank space at the bottom, as
@ -1115,4 +1192,3 @@ def main(args=sys.argv):
if __name__ == '__main__':
sys.exit(main())

View File

@ -11,7 +11,7 @@ from threading import Thread
from calibre.constants import iswindows, get_windows_username, islinux
ADDRESS = None
ADDRESS = VADDRESS = None
def eintr_retry_call(func, *args, **kwargs):
while True:
@ -48,10 +48,39 @@ def gui_socket_address():
ADDRESS = os.path.join(tmp, user+'-calibre-gui.socket')
return ADDRESS
def viewer_socket_address():
global VADDRESS
if VADDRESS is None:
if iswindows:
VADDRESS = r'\\.\pipe\CalibreViewer'
try:
user = get_windows_username()
except:
user = None
if user:
from calibre.utils.filenames import ascii_filename
user = ascii_filename(user).replace(' ', '_')
if user:
VADDRESS += '-' + user[:100] + 'x'
else:
user = os.environ.get('USER', '')
if not user:
user = os.path.basename(os.path.expanduser('~'))
if islinux:
VADDRESS = (u'\0%s-calibre-viewer.socket' % user).encode('ascii')
else:
from tempfile import gettempdir
tmp = gettempdir()
VADDRESS = os.path.join(tmp, user+'-calibre-viewer.socket')
return VADDRESS
class RC(Thread):
def __init__(self, print_error=True):
def __init__(self, print_error=True, socket_address=None):
self.print_error = print_error
self.socket_address = socket_address or gui_socket_address()
Thread.__init__(self)
self.conn = None
self.daemon = True
@ -60,11 +89,9 @@ class RC(Thread):
from multiprocessing.connection import Client
self.done = False
try:
self.conn = Client(gui_socket_address())
self.conn = Client(self.socket_address)
self.done = True
except:
if self.print_error:
import traceback
traceback.print_exc()