Files
Lector/lector/widgets.py
BasioMeusPuga 406ca0485f Position setting should work all the time now
Learn not to swear so much at the screen
Cover icons in the tab bar
Shift Scan Library button from the Library tab to the Library toolbar
2018-03-29 01:45:58 +05:30

1076 lines
41 KiB
Python

#!usr/bin/env python3
# This file is a part of Lector, a Qt based ebook reader
# Copyright (C) 2017 BasioMeusPuga
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# TODO
# Reading modes
# Double page, Continuous etc
# Especially for comics
# Remove variables that have anything to do with scroll position
import os
import uuid
import zipfile
try:
import popplerqt5
except ImportError:
pass
from PyQt5 import QtWidgets, QtGui, QtCore
from lector.rarfile import rarfile
from lector.models import BookmarkProxyModel
from lector.delegates import BookmarkDelegate
from lector.threaded import BackGroundCacheRefill
from lector.sorter import resize_image
class Tab(QtWidgets.QWidget):
def __init__(self, metadata, main_window, parent=None):
super(Tab, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.first_run = True
self.main_window = main_window
self.metadata = metadata # Save progress data into this dictionary
self.are_we_doing_images_only = self.metadata['images_only']
self.is_fullscreen = False
self.masterLayout = QtWidgets.QHBoxLayout(self)
self.masterLayout.setContentsMargins(0, 0, 0, 0)
self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime()
if self.metadata['position']:
if self.metadata['position']['is_read']:
self.generate_position(True)
current_chapter = self.metadata['position']['current_chapter']
else:
self.generate_position()
current_chapter = 1
chapter_content = self.metadata['content'][current_chapter - 1][1]
# The content display widget is, by default a QTextBrowser.
# In case the incoming data is only images
# such as in the case of comic book files,
# we want a QGraphicsView widget doing all the heavy lifting
# instead of a QTextBrowser
if self.are_we_doing_images_only: # Boolean
self.contentView = PliantQGraphicsView(
self.metadata['path'], self.main_window, self)
self.contentView.loadImage(chapter_content)
else:
self.contentView = PliantQTextBrowser(self.main_window, self)
relative_path_root = os.path.join(
self.main_window.temp_dir.path(), self.metadata['hash'])
relative_paths = []
for i in os.walk(relative_path_root):
# TODO
# Rename the .css files to something else here and keep
# a record of them
# Currently, I'm just removing them for the sake of simplicity
for j in i[2]:
file_extension = os.path.splitext(j)[1]
if file_extension == '.css':
file_path = os.path.join(i[0], j)
os.remove(file_path)
relative_paths.append(os.path.join(relative_path_root, i[0]))
self.contentView.setSearchPaths(relative_paths)
self.contentView.setOpenLinks(False) # TODO Change this when HTML navigation works
self.contentView.setHtml(chapter_content)
self.contentView.setReadOnly(True)
self.hiddenButton = QtWidgets.QToolButton(self)
self.hiddenButton.setVisible(False)
self.hiddenButton.clicked.connect(self.set_cursor_position)
self.hiddenButton.animateClick(50)
# The following are common to both the text browser and
# the graphics view
self.contentView.setFrameShape(QtWidgets.QFrame.NoFrame)
self.contentView.setObjectName('contentView')
self.contentView.verticalScrollBar().setSingleStep(
self.main_window.settings['scroll_speed'])
if self.main_window.settings['hide_scrollbars']:
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.contentView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
else:
self.contentView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.contentView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
# See bookmark availability
if not self.metadata['bookmarks']:
self.metadata['bookmarks'] = {}
# Create the dock widget for context specific display
self.dockWidget = PliantDockWidget(self.main_window, self.contentView)
self.dockWidget.setWindowTitle(self._translate('Tab', 'Bookmarks'))
self.dockWidget.setFeatures(QtWidgets.QDockWidget.DockWidgetClosable)
self.dockWidget.hide()
self.dockListView = QtWidgets.QListView(self.dockWidget)
self.dockListView.setResizeMode(QtWidgets.QListWidget.Adjust)
self.dockListView.setMaximumWidth(350)
self.dockListView.setItemDelegate(
BookmarkDelegate(self.main_window, self.dockListView))
self.dockListView.setUniformItemSizes(True)
self.dockListView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.dockListView.customContextMenuRequested.connect(
self.generate_bookmark_context_menu)
self.dockListView.clicked.connect(self.navigate_to_bookmark)
self.dockWidget.setWidget(self.dockListView)
self.bookmark_model = QtGui.QStandardItemModel(self)
self.proxy_model = BookmarkProxyModel(self)
self.generate_bookmark_model()
self.generate_keyboard_shortcuts()
self.masterLayout.addWidget(self.contentView)
self.masterLayout.addWidget(self.dockWidget)
self.dockWidget.setFloating(True)
self.dockWidget.setWindowOpacity(.95)
title = self.metadata['title']
self.main_window.tabWidget.addTab(self, title)
# TODO
# Show cover image as tooltip text
this_tab_index = self.main_window.tabWidget.indexOf(self)
cover_icon = QtGui.QPixmap()
cover_icon.loadFromData(self.metadata['cover'])
self.main_window.tabWidget.setTabIcon(
this_tab_index, QtGui.QIcon(cover_icon))
# Hide mouse cursor timer
self.mouse_hide_timer = QtCore.QTimer()
self.mouse_hide_timer.setSingleShot(True)
self.mouse_hide_timer.timeout.connect(self.hide_mouse)
self.contentView.setFocus()
def update_last_accessed_time(self):
self.metadata['last_accessed'] = QtCore.QDateTime().currentDateTime()
start_index = self.main_window.lib_ref.view_model.index(0, 0)
matching_item = self.main_window.lib_ref.view_model.match(
start_index,
QtCore.Qt.UserRole + 6,
self.metadata['hash'],
1, QtCore.Qt.MatchExactly)
try:
self.main_window.lib_ref.view_model.setData(
matching_item[0], self.metadata['last_accessed'], QtCore.Qt.UserRole + 12)
except IndexError: # The file has been deleted
pass
def set_cursor_position(self, switch_widgets=True, search_data=None):
# if self.sender().objectName() == 'tabWidget' and self.first_run:
# return
# self.first_run = False
# ^^^ I have NO IDEA why this might be needed or how it works
if switch_widgets:
previous_widget = self.main_window.tabWidget.currentWidget()
self.main_window.tabWidget.setCurrentWidget(self)
try:
required_position = self.metadata['position']['cursor_position']
if search_data:
required_position = search_data[1]
# TODO
# Remove this at the time when a library rebuild is
# finally required
# Bookmarks will have to be regenerated in the meantime
if isinstance(required_position, str):
return
# This is needed so that the line we want is
# always at the top of the window
self.contentView.verticalScrollBar().setValue(
self.contentView.verticalScrollBar().maximum())
# textCursor() RETURNS a copy of the textcursor
cursor = self.contentView.textCursor()
cursor.setPosition(
required_position, QtGui.QTextCursor.MoveAnchor)
self.contentView.setTextCursor(cursor)
self.contentView.ensureCursorVisible()
except KeyError:
pass
if switch_widgets:
self.main_window.tabWidget.setCurrentWidget(previous_widget)
def generate_position(self, is_read=False):
total_chapters = len(self.metadata['content'])
current_chapter = 1
scroll_value = 0
if is_read:
current_chapter = total_chapters
scroll_value = 1
# TODO
# Use this to generate position
# Generate block count @ time of first read
# Blocks are indexed from 0 up
blocks_per_chapter = []
total_blocks = 0
if not self.are_we_doing_images_only:
for i in self.metadata['content']:
chapter_html = i[1]
textDocument = QtGui.QTextDocument(None)
textDocument.setHtml(chapter_html)
block_count = textDocument.blockCount()
blocks_per_chapter.append(block_count)
total_blocks += block_count
self.metadata['position'] = {
'current_chapter': current_chapter,
'total_chapters': total_chapters,
'blocks_per_chapter': blocks_per_chapter,
'total_blocks': total_blocks,
'scroll_value': scroll_value,
'is_read': is_read,
'cursor_position': 0}
def generate_keyboard_shortcuts(self):
self.ksNextChapter = QtWidgets.QShortcut(
QtGui.QKeySequence('Right'), self.contentView)
self.ksNextChapter.setObjectName('nextChapter')
self.ksNextChapter.activated.connect(self.sneaky_change)
self.ksPrevChapter = QtWidgets.QShortcut(
QtGui.QKeySequence('Left'), self.contentView)
self.ksPrevChapter.setObjectName('prevChapter')
self.ksPrevChapter.activated.connect(self.sneaky_change)
self.ksGoFullscreen = QtWidgets.QShortcut(
QtGui.QKeySequence('F11'), self.contentView)
self.ksGoFullscreen.activated.connect(self.go_fullscreen)
self.ksExitFullscreen = QtWidgets.QShortcut(
QtGui.QKeySequence('Escape'), self.contentView)
self.ksExitFullscreen.setContext(QtCore.Qt.ApplicationShortcut)
self.ksExitFullscreen.activated.connect(self.exit_fullscreen)
self.ksToggleBookMarks = QtWidgets.QShortcut(
QtGui.QKeySequence('Ctrl+B'), self.contentView)
self.ksToggleBookMarks.activated.connect(self.toggle_bookmarks)
def go_fullscreen(self):
if self.contentView.windowState() == QtCore.Qt.WindowFullScreen:
self.exit_fullscreen()
return
if not self.are_we_doing_images_only:
self.contentView.record_scroll_position()
self.contentView.setWindowFlags(QtCore.Qt.Window)
self.contentView.setWindowState(QtCore.Qt.WindowFullScreen)
self.contentView.show()
self.main_window.hide()
if not self.are_we_doing_images_only:
self.hiddenButton.animateClick(100)
self.is_fullscreen = True
def exit_fullscreen(self):
if self.dockWidget.isVisible():
self.dockWidget.setVisible(False)
return
if not self.are_we_doing_images_only:
self.contentView.record_scroll_position()
self.main_window.show()
self.contentView.setWindowFlags(QtCore.Qt.Widget)
self.contentView.setWindowState(QtCore.Qt.WindowNoState)
self.contentView.show()
self.is_fullscreen = False
if not self.are_we_doing_images_only:
self.hiddenButton.animateClick(100)
# Hide the view modification buttons in case they're visible
self.main_window.bookToolBar.customize_view_off()
# Exit distraction free mode too
if not self.main_window.settings['show_bars']:
self.main_window.toggle_distraction_free()
self.contentView.setFocus()
def change_chapter_tocBox(self):
chapter_number = self.main_window.bookToolBar.tocBox.currentIndex()
required_content = self.metadata['content'][chapter_number][1]
if self.are_we_doing_images_only:
self.contentView.loadImage(required_content)
else:
self.contentView.clear()
self.contentView.setHtml(required_content)
def format_view(self, font, font_size, foreground,
background, padding, line_spacing,
text_alignment):
if self.are_we_doing_images_only:
# Tab color does not need to be set separately in case
# no padding is set for the viewport of a QGraphicsView
# and image resizing in done in the pixmap
my_qbrush = QtGui.QBrush(QtCore.Qt.SolidPattern)
my_qbrush.setColor(background)
self.contentView.setBackgroundBrush(my_qbrush)
self.contentView.resizeEvent()
else:
self.contentView.setStyleSheet(
"QTextEdit {{font-family: {0}; font-size: {1}px; color: {2}; background-color: {3}}}".format(
font, font_size, foreground.name(), background.name()))
# Line spacing
# Set line spacing per a block format
# This is proportional line spacing so assume a divisor of 100
block_format = QtGui.QTextBlockFormat()
block_format.setLineHeight(
line_spacing, QtGui.QTextBlockFormat.ProportionalHeight)
block_format.setTextIndent(50)
# Give options for alignment
alignment_dict = {
'left': QtCore.Qt.AlignLeft,
'right': QtCore.Qt.AlignRight,
'center': QtCore.Qt.AlignCenter,
'justify': QtCore.Qt.AlignJustify}
current_index = self.main_window.bookToolBar.tocBox.currentIndex()
if current_index == 0:
block_format.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
else:
block_format.setAlignment(alignment_dict[text_alignment])
# Also for padding
# Using setViewPortMargins for this disables scrolling in the margins
block_format.setLeftMargin(padding)
block_format.setRightMargin(padding)
this_cursor = self.contentView.textCursor()
this_cursor.movePosition(QtGui.QTextCursor.Start, 0, 1)
# Iterate over the entire document block by block
# The document ends when the cursor position can no longer be incremented
while True:
old_position = this_cursor.position()
this_cursor.mergeBlockFormat(block_format)
this_cursor.movePosition(QtGui.QTextCursor.NextBlock, 0, 1)
new_position = this_cursor.position()
if old_position == new_position:
break
def toggle_bookmarks(self):
if self.dockWidget.isVisible():
self.dockWidget.hide()
else:
self.dockWidget.show()
def add_bookmark(self):
# TODO
# Start dockListView.edit(index) when something new is added
identifier = uuid.uuid4().hex[:10]
description = self._translate('Tab', 'New bookmark')
if self.are_we_doing_images_only:
chapter = self.metadata['position']['current_chapter']
search_data = (0, None)
else:
chapter, scroll_position, visible_text = self.contentView.record_scroll_position(True)
search_data = (scroll_position, visible_text)
self.metadata['bookmarks'][identifier] = {
'chapter': chapter,
'search_data': search_data,
'description': description}
self.add_bookmark_to_model(
description, chapter, search_data, identifier)
self.dockWidget.setVisible(True)
def add_bookmark_to_model(self, description, chapter, search_data, identifier):
bookmark = QtGui.QStandardItem()
bookmark.setData(description, QtCore.Qt.DisplayRole)
bookmark.setData(chapter, QtCore.Qt.UserRole)
bookmark.setData(search_data, QtCore.Qt.UserRole + 1)
bookmark.setData(identifier, QtCore.Qt.UserRole + 2)
self.bookmark_model.appendRow(bookmark)
self.update_bookmark_proxy_model()
def navigate_to_bookmark(self, index):
if not index.isValid():
return
chapter = self.proxy_model.data(index, QtCore.Qt.UserRole)
search_data = self.proxy_model.data(index, QtCore.Qt.UserRole + 1)
self.main_window.bookToolBar.tocBox.setCurrentIndex(chapter - 1)
if not self.are_we_doing_images_only:
self.set_cursor_position(False, search_data)
def generate_bookmark_model(self):
# TODO
# Sorting is not working correctly
for i in self.metadata['bookmarks'].items():
self.add_bookmark_to_model(
i[1]['description'],
i[1]['chapter'],
i[1]['search_data'],
i[0])
self.generate_bookmark_proxy_model()
def generate_bookmark_proxy_model(self):
self.proxy_model.setSourceModel(self.bookmark_model)
self.proxy_model.setSortCaseSensitivity(False)
self.proxy_model.setSortRole(QtCore.Qt.UserRole)
self.dockListView.setModel(self.proxy_model)
def update_bookmark_proxy_model(self):
self.proxy_model.invalidateFilter()
self.proxy_model.setFilterParams(
self.main_window.bookToolBar.searchBar.text())
self.proxy_model.setFilterFixedString(
self.main_window.bookToolBar.searchBar.text())
def generate_bookmark_context_menu(self, position):
index = self.dockListView.indexAt(position)
if not index.isValid():
return
bookmark_menu = QtWidgets.QMenu()
editAction = bookmark_menu.addAction(
self.main_window.QImageFactory.get_image('edit-rename'),
self._translate('Tab', 'Edit'))
deleteAction = bookmark_menu.addAction(
self.main_window.QImageFactory.get_image('trash-empty'),
self._translate('Tab', 'Delete'))
action = bookmark_menu.exec_(
self.dockListView.mapToGlobal(position))
if action == editAction:
self.dockListView.edit(index)
if action == deleteAction:
row = index.row()
delete_uuid = self.bookmark_model.item(row).data(QtCore.Qt.UserRole + 2)
self.metadata['bookmarks'].pop(delete_uuid)
self.bookmark_model.removeRow(index.row())
def hide_mouse(self):
self.contentView.viewport().setCursor(QtCore.Qt.BlankCursor)
def sneaky_change(self):
direction = -1
if self.sender().objectName() == 'nextChapter':
direction = 1
self.contentView.common_functions.change_chapter(
direction, True)
def sneaky_exit(self):
self.contentView.hide()
self.main_window.closeEvent()
class PliantQGraphicsView(QtWidgets.QGraphicsView):
def __init__(self, filepath, main_window, parent=None):
super(PliantQGraphicsView, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
self.parent = parent
self.main_window = main_window
self.qimage = None # Will be needed to resize pdf
self.image_pixmap = None
self.image_cache = [None for _ in range(4)]
self.thread = None
self.filepath = filepath
self.filetype = os.path.splitext(self.filepath)[1][1:]
if self.filetype == 'cbz':
self.book = zipfile.ZipFile(self.filepath)
elif self.filetype == 'cbr':
self.book = rarfile.RarFile(self.filepath)
elif self.filetype == 'pdf':
self.book = popplerqt5.Poppler.Document.load(self.filepath)
self.book.setRenderHint(
popplerqt5.Poppler.Document.Antialiasing
and popplerqt5.Poppler.Document.TextAntialiasing)
self.common_functions = PliantWidgetsCommonFunctions(
self, self.main_window)
# TODO
# Image panning with mouse
self.ignore_wheel_event = False
self.ignore_wheel_event_number = 0
self.setMouseTracking(True)
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(
self.generate_graphicsview_context_menu)
def loadImage(self, current_page):
# TODO
# For double page view: 1 before, 1 after
all_pages = [i[1] for i in self.parent.metadata['content']]
def load_page(current_page):
image_pixmap = QtGui.QPixmap()
if self.filetype in ('cbz', 'cbr'):
page_data = self.book.read(current_page)
image_pixmap.loadFromData(page_data)
elif self.filetype == 'pdf':
page_data = self.book.page(current_page)
page_qimage = page_data.renderToImage(350, 350)
image_pixmap.convertFromImage(page_qimage)
return image_pixmap
def generate_image_cache(current_page):
print('Building image cache')
current_page_index = all_pages.index(current_page)
for i in (-1, 0, 1, 2):
try:
this_page = all_pages[current_page_index + i]
this_pixmap = load_page(this_page)
self.image_cache[i + 1] = (this_page, this_pixmap)
except IndexError:
self.image_cache[i + 1] = None
def refill_cache(remove_value):
# Do NOT put a parent in here or the mother of all
# memory leaks will result
self.thread = BackGroundCacheRefill(
self.image_cache, remove_value,
self.filetype, self.book, all_pages)
self.thread.finished.connect(overwrite_cache)
self.thread.start()
def overwrite_cache():
self.image_cache = self.thread.image_cache
def check_cache(current_page):
for i in self.image_cache:
if i:
if i[0] == current_page:
return_pixmap = i[1]
refill_cache(i)
return return_pixmap
# No return happened so the image isn't in the cache
generate_image_cache(current_page)
if self.main_window.settings['caching_enabled']:
return_pixmap = None
while not return_pixmap:
return_pixmap = check_cache(current_page)
else:
return_pixmap = load_page(current_page)
self.image_pixmap = return_pixmap
self.resizeEvent()
def resizeEvent(self, *args):
if not self.image_pixmap:
return
zoom_mode = self.main_window.comic_profile['zoom_mode']
padding = self.main_window.comic_profile['padding']
if zoom_mode == 'fitWidth':
available_width = self.viewport().width()
image_pixmap = self.image_pixmap.scaledToWidth(
available_width, QtCore.Qt.SmoothTransformation)
elif zoom_mode == 'originalSize':
image_pixmap = self.image_pixmap
new_padding = (self.viewport().width() - image_pixmap.width()) // 2
if new_padding < 0: # The image is larger than the viewport
self.main_window.comic_profile['padding'] = 0
else:
self.main_window.comic_profile['padding'] = new_padding
elif zoom_mode == 'bestFit':
available_width = self.viewport().width()
available_height = self.viewport().height()
image_pixmap = self.image_pixmap.scaled(
available_width, available_height,
QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
self.main_window.comic_profile['padding'] = (
self.viewport().width() - image_pixmap.width()) // 2
elif zoom_mode == 'manualZoom':
available_width = self.viewport().width() - 2 * padding
image_pixmap = self.image_pixmap.scaledToWidth(
available_width, QtCore.Qt.SmoothTransformation)
graphics_scene = QtWidgets.QGraphicsScene()
graphics_scene.addPixmap(image_pixmap)
self.setScene(graphics_scene)
self.show()
def wheelEvent(self, event):
self.common_functions.wheelEvent(event, True)
def keyPressEvent(self, event):
vertical = self.verticalScrollBar().value()
maximum = self.verticalScrollBar().maximum()
def scroller(increment, move_forward=True):
if move_forward:
if vertical == maximum:
self.common_functions.change_chapter(1, True)
else:
next_val = vertical + increment
if next_val >= .95 * maximum:
next_val = maximum
self.verticalScrollBar().setValue(next_val)
else:
if vertical == 0:
self.common_functions.change_chapter(-1, False)
else:
next_val = vertical - increment
if next_val <= .05 * maximum:
next_val = 0
self.verticalScrollBar().setValue(next_val)
small_increment = maximum // 4
big_increment = maximum // 2
if event.key() == QtCore.Qt.Key_Up:
scroller(small_increment, False)
if event.key() == QtCore.Qt.Key_Down:
scroller(small_increment)
if event.key() == QtCore.Qt.Key_Space:
scroller(big_increment)
view_modification_keys = (
QtCore.Qt.Key_Plus, QtCore.Qt.Key_Minus, QtCore.Qt.Key_Equal,
QtCore.Qt.Key_B, QtCore.Qt.Key_W, QtCore.Qt.Key_O)
if event.key() in view_modification_keys:
self.main_window.modify_comic_view(event.key())
def mouseMoveEvent(self, *args):
self.viewport().setCursor(QtCore.Qt.ArrowCursor)
self.parent.mouse_hide_timer.start(3000)
def generate_graphicsview_context_menu(self, position):
contextMenu = QtWidgets.QMenu()
saveAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('filesaveas'),
self._translate('PliantQGraphicsView', 'Save page as...'))
fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQGraphicsView', 'Exit fullscreen'))
else:
if self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQGraphicsView', 'Distraction Free mode')
else:
distraction_free_prompt = self._translate(
'PliantQGraphicsView', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
viewSubMenu = contextMenu.addMenu('View')
viewSubMenu.setIcon(
self.main_window.QImageFactory.get_image('mail-thread-watch'))
zoominAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-in'),
self._translate('PliantQGraphicsView', 'Zoom in (+)'))
zoomoutAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-out'),
self._translate('PliantQGraphicsView', 'Zoom out (-)'))
fitWidthAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-fit-width'),
self._translate('PliantQGraphicsView', 'Fit width (W)'))
bestFitAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-fit-best'),
self._translate('PliantQGraphicsView', 'Best fit (B)'))
originalSizeAction = viewSubMenu.addAction(
self.main_window.QImageFactory.get_image('zoom-original'),
self._translate('PliantQGraphicsView', 'Original size (O)'))
bookmarksToggleAction = 'Latin quote 2. Electric Boogaloo.'
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
bookmarksToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('bookmarks'),
self._translate('PliantQGraphicsView', 'Bookmarks'))
self.common_functions.generate_combo_box_action(contextMenu)
action = contextMenu.exec_(self.sender().mapToGlobal(position))
if action == saveAction:
dialog_prompt = self._translate('Main_UI', 'Save page as...')
extension_string = self._translate('Main_UI', 'Images')
save_file = QtWidgets.QFileDialog.getSaveFileName(
self, dialog_prompt, self.main_window.settings['last_open_path'],
f'{extension_string} (*.png *.jpg *.bmp)')
if save_file:
self.image_pixmap.save(save_file[0])
if action == bookmarksToggleAction:
self.parent.toggle_bookmarks()
if action == dfToggleAction:
self.main_window.toggle_distraction_free()
if action == fsToggleAction:
self.parent.exit_fullscreen()
view_action_dict = {
zoominAction: QtCore.Qt.Key_Plus,
zoomoutAction: QtCore.Qt.Key_Minus,
fitWidthAction: QtCore.Qt.Key_W,
bestFitAction: QtCore.Qt.Key_B,
originalSizeAction: QtCore.Qt.Key_O}
if action in view_action_dict:
self.main_window.modify_comic_view(view_action_dict[action])
def closeEvent(self, *args):
# In case the program is closed when a contentView is fullscreened
self.main_window.closeEvent()
class PliantQTextBrowser(QtWidgets.QTextBrowser):
def __init__(self, main_window, parent=None):
super(PliantQTextBrowser, self).__init__(parent)
self._translate = QtCore.QCoreApplication.translate
self.parent = parent
self.main_window = main_window
self.common_functions = PliantWidgetsCommonFunctions(
self, self.main_window)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(
self.generate_textbrowser_context_menu)
self.setMouseTracking(True)
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
self.verticalScrollBar().sliderMoved.connect(
self.record_scroll_position)
self.ignore_wheel_event = False
self.ignore_wheel_event_number = 0
def wheelEvent(self, event):
self.record_scroll_position()
self.common_functions.wheelEvent(event, False)
def keyPressEvent(self, event):
QtWidgets.QTextEdit.keyPressEvent(self, event)
if event.key() == QtCore.Qt.Key_Space:
if self.verticalScrollBar().value() == self.verticalScrollBar().maximum():
self.common_functions.change_chapter(1, True)
else:
self.set_top_line_cleanly()
def set_top_line_cleanly(self):
# Find the cursor position of the top line and move to it
find_cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
find_cursor.movePosition(
find_cursor.position(), QtGui.QTextCursor.KeepAnchor)
self.setTextCursor(find_cursor)
self.ensureCursorVisible()
def record_scroll_position(self, return_as_bookmark=False):
self.parent.metadata['position']['is_read'] = False
vertical = self.verticalScrollBar().value()
maximum = self.verticalScrollBar().maximum()
self.parent.metadata['position']['scroll_value'] = 1
if maximum != 0:
self.parent.metadata['position']['scroll_value'] = (vertical / maximum)
cursor = self.cursorForPosition(QtCore.QPoint(0, 0))
cursor_position = cursor.position()
if return_as_bookmark:
return (self.parent.metadata['position']['current_chapter'],
self.parent.metadata['position']['scroll_value'],
cursor_position)
else:
self.parent.metadata['position']['cursor_position'] = cursor_position
def generate_textbrowser_context_menu(self, position):
selected_word = self.textCursor().selection()
selected_word = selected_word.toPlainText()
contextMenu = QtWidgets.QMenu()
# The following cannot be None because a click
# outside the menu means that the action variable is None.
defineAction = fsToggleAction = dfToggleAction = 'Caesar si viveret, ad remum dareris'
if selected_word and selected_word != '':
selected_word = selected_word.split()[0]
define_string = self._translate('PliantQTextBrowser', 'Define')
defineAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-readermode'),
f'{define_string} "{selected_word}"')
searchAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('search'),
self._translate('PliantQTextBrowser', 'Search'))
if self.parent.is_fullscreen:
fsToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('view-fullscreen'),
self._translate('PliantQTextBrowser', 'Exit fullscreen'))
else:
if self.main_window.settings['show_bars']:
distraction_free_prompt = self._translate(
'PliantQTextBrowser', 'Distraction Free mode')
else:
distraction_free_prompt = self._translate(
'PliantQTextBrowser', 'Exit Distraction Free mode')
dfToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('visibility'),
distraction_free_prompt)
bookmarksToggleAction = 'Latin quote 2. Electric Boogaloo.'
if not self.main_window.settings['show_bars'] or self.parent.is_fullscreen:
bookmarksToggleAction = contextMenu.addAction(
self.main_window.QImageFactory.get_image('bookmarks'),
self._translate('PliantQTextBrowser', 'Bookmarks'))
self.common_functions.generate_combo_box_action(contextMenu)
action = contextMenu.exec_(self.sender().mapToGlobal(position))
if action == defineAction:
self.main_window.definitionDialog.find_definition(selected_word)
if action == searchAction:
self.main_window.bookToolBar.searchBar.setFocus()
if action == bookmarksToggleAction:
self.parent.toggle_bookmarks()
if action == fsToggleAction:
self.parent.exit_fullscreen()
if action == dfToggleAction:
self.main_window.toggle_distraction_free()
def closeEvent(self, *args):
self.main_window.closeEvent()
def mouseMoveEvent(self, event):
self.viewport().setCursor(QtCore.Qt.IBeamCursor)
self.parent.mouse_hide_timer.start(3000)
QtWidgets.QTextBrowser.mouseMoveEvent(self, event)
class PliantWidgetsCommonFunctions():
def __init__(self, parent_widget, main_window):
self.pw = parent_widget
self.main_window = main_window
def wheelEvent(self, event, are_we_doing_images_only):
ignore_events = 20
if are_we_doing_images_only:
ignore_events = 10
if self.pw.ignore_wheel_event:
self.pw.ignore_wheel_event_number += 1
if self.pw.ignore_wheel_event_number > ignore_events:
self.pw.ignore_wheel_event = False
self.pw.ignore_wheel_event_number = 0
return
if are_we_doing_images_only:
QtWidgets.QGraphicsView.wheelEvent(self.pw, event)
else:
QtWidgets.QTextBrowser.wheelEvent(self.pw, event)
# Since this is a delta on a mouse move event, it cannot ever be 0
vertical_pdelta = event.pixelDelta().y()
if vertical_pdelta > 0:
moving_up = True
elif vertical_pdelta < 0:
moving_up = False
if abs(vertical_pdelta) > 80: # Adjust sensitivity here
# Implies that no scrollbar movement is possible
if self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum() == 0:
if moving_up:
self.change_chapter(-1)
else:
self.change_chapter(1)
# Implies that the scrollbar is at the bottom
elif self.pw.verticalScrollBar().value() == self.pw.verticalScrollBar().maximum():
if not moving_up:
self.change_chapter(1)
# Implies scrollbar is at the top
elif self.pw.verticalScrollBar().value() == 0:
if moving_up:
self.change_chapter(-1)
def change_chapter(self, direction, was_button_pressed=None):
current_toc_index = self.main_window.bookToolBar.tocBox.currentIndex()
max_toc_index = self.main_window.bookToolBar.tocBox.count() - 1
if (current_toc_index < max_toc_index and direction == 1) or (
current_toc_index > 0 and direction == -1):
self.main_window.bookToolBar.tocBox.setCurrentIndex(
current_toc_index + direction)
# Set page position depending on if the chapter number is increasing or decreasing
if direction == 1 or was_button_pressed:
self.pw.verticalScrollBar().setValue(0)
else:
self.pw.verticalScrollBar().setValue(
self.pw.verticalScrollBar().maximum())
if not was_button_pressed:
self.pw.ignore_wheel_event = True
def generate_combo_box_action(self, contextMenu):
contextMenu.addSeparator()
toc_combobox = QtWidgets.QComboBox()
toc_data = [i[0] for i in self.pw.parent.metadata['content']]
toc_combobox.addItems(toc_data)
toc_combobox.setCurrentIndex(
self.pw.main_window.bookToolBar.tocBox.currentIndex())
toc_combobox.currentIndexChanged.connect(
self.pw.main_window.bookToolBar.tocBox.setCurrentIndex)
comboboxAction = QtWidgets.QWidgetAction(self.pw)
comboboxAction.setDefaultWidget(toc_combobox)
contextMenu.addAction(comboboxAction)
class PliantDockWidget(QtWidgets.QDockWidget):
def __init__(self, main_window, contentView, parent=None):
super(PliantDockWidget, self).__init__()
self.main_window = main_window
self.contentView = contentView
def showEvent(self, event):
viewport_height = self.contentView.viewport().size().height()
viewport_topRight = self.contentView.mapToGlobal(
self.contentView.viewport().rect().topRight())
desktop_size = QtWidgets.QDesktopWidget().screenGeometry()
dock_width = desktop_size.width() // 5.5
dock_x = viewport_topRight.x() - dock_width + 1
dock_y = viewport_topRight.y() + (viewport_height * .10)
dock_height = viewport_height * .80
self.setGeometry(dock_x, dock_y, dock_width, dock_height)
self.main_window.bookToolBar.bookmarkButton.setChecked(True)
def hideEvent(self, event):
self.main_window.bookToolBar.bookmarkButton.setChecked(False)
class PliantQGraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(PliantQGraphicsScene, self).__init__(parent)
self.parent = parent
self._translate = QtCore.QCoreApplication.translate
def mouseReleaseEvent(self, event):
self.parent.previous_position = self.parent.pos()
image_files = '*.jpg *.png'
dialog_prompt = self._translate('PliantQGraphicsScene', 'Select new cover')
images_string = self._translate('PliantQGraphicsScene', 'Images')
new_cover = QtWidgets.QFileDialog.getOpenFileName(
None, dialog_prompt, self.parent.parent.settings['last_open_path'],
f'{images_string} ({image_files})')[0]
if not new_cover:
self.parent.show()
return
with open(new_cover, 'rb') as cover_ref:
cover_bytes = cover_ref.read()
resized_cover = resize_image(cover_bytes)
self.parent.cover_for_database = resized_cover
cover_pixmap = QtGui.QPixmap()
cover_pixmap.load(new_cover)
cover_pixmap = cover_pixmap.scaled(
140, 205, QtCore.Qt.IgnoreAspectRatio)
self.parent.load_cover(cover_pixmap, True)
self.parent.show()