# Copyright 2009 One Laptop Per Child # Author: Sayamindu Dasgupta # # 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 2 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, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from gi.repository import Gtk from gi.repository import GObject from gi.repository import Gdk import widgets import logging import os.path import math import shutil import BeautifulSoup from epub import _Epub from jobs import _JobPaginator as _Paginator LOADING_HTML = '''

Loading...

''' class _View(Gtk.HBox): __gproperties__ = { 'scale': (GObject.TYPE_FLOAT, 'the zoom level', 'the zoom level of the widget', 0.5, 4.0, 1.0, GObject.PARAM_READWRITE), } __gsignals__ = { 'page-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ([int, int])), 'selection-changed': (GObject.SignalFlags.RUN_FIRST, GObject.TYPE_NONE, ([])), } def __init__(self): GObject.threads_init() Gtk.HBox.__init__(self) self.connect("destroy", self._destroy_cb) self._ready = False self._paginator = None self._loaded_page = -1 self._file_loaded = True #self._old_scrollval = -1 self._loaded_filename = None self._pagecount = -1 self.__going_fwd = True self.__going_back = False self.__page_changed = False self._has_selection = False self.scale = 1.0 self._epub = None self._findjob = None self.__in_search = False self.__search_fwd = True self._filelist = None self._internal_link = None self._sw = Gtk.ScrolledWindow() self._view = widgets._WebView() self._view.load_string(LOADING_HTML, 'text/html', 'utf-8', '/') settings = self._view.get_settings() settings.props.default_font_family = 'DejaVu LGC Serif' settings.props.enable_plugins = False settings.props.default_encoding = 'utf-8' self._view.connect('load-finished', self._view_load_finished_cb) self._view.connect('scroll-event', self._view_scroll_event_cb) self._view.connect('key-press-event', self._view_keypress_event_cb) self._view.connect('selection-changed', self._view_selection_changed_cb) self._view.connect_after('populate-popup', self._view_populate_popup_cb) self._view.connect('touch-change-page', self.__touch_page_changed_cb) self._sw.add(self._view) self._v_vscrollbar = self._sw.get_vscrollbar() self._v_scrollbar_value_changed_cb_id = \ self._v_vscrollbar.connect('value-changed', \ self._v_scrollbar_value_changed_cb) self._scrollbar = Gtk.VScrollbar() self._scrollbar_change_value_cb_id = \ self._scrollbar.connect('change-value', \ self._scrollbar_change_value_cb) overlay = Gtk.Overlay() hbox = Gtk.HBox() overlay.add(hbox) hbox.add(self._sw) self._scrollbar.props.halign = Gtk.Align.END self._scrollbar.props.valign = Gtk.Align.FILL overlay.add_overlay(self._scrollbar) self.pack_start(overlay, True, True, 0) self._view.set_can_default(True) self._view.set_can_focus(True) def map_cp(widget): widget.setup_touch() widget.disconnect(self._setup_handle) self._setup_handle = self._view.connect('map', map_cp) def set_document(self, epubdocumentinstance): ''' Sets document (should be a Epub instance) ''' self._epub = epubdocumentinstance GObject.idle_add(self._paginate) def do_get_property(self, property): if property.name == 'has-selection': return self._has_selection elif property.name == 'scale': return self.scale else: raise AttributeError('unknown property %s' % property.name) def do_set_property(self, property, value): if property.name == 'scale': self.__set_zoom(value) else: raise AttributeError('unknown property %s' % property.name) def get_has_selection(self): ''' Returns True if any part of the content is selected ''' return self._view.can_copy_clipboard() def get_zoom(self): ''' Returns the current zoom level ''' return self.get_property('scale') * 100.0 def set_zoom(self, value): ''' Sets the current zoom level ''' scrollbar_pos = self.get_vertical_pos() self._view.set_zoom_level(value / 100.0) self.set_vertical_pos(scrollbar_pos) def _get_scale(self): ''' Returns the current zoom level ''' return self.get_property('scale') def _set_scale(self, value): ''' Sets the current zoom level ''' self.set_property('scale', value) def zoom_in(self): ''' Zooms in (increases zoom level by 0.1) ''' if self.can_zoom_in(): scrollbar_pos = self.get_vertical_pos() self._set_scale(self._get_scale() + 0.1) self.set_vertical_pos(scrollbar_pos) return True else: return False def zoom_out(self): ''' Zooms out (decreases zoom level by 0.1) ''' if self.can_zoom_out(): scrollbar_pos = self.get_vertical_pos() self._set_scale(self._get_scale() - 0.1) self.set_vertical_pos(scrollbar_pos) return True else: return False def get_vertical_pos(self): """ Used to save the scrolled position and restore when needed """ return self._v_vscrollbar.get_adjustment().get_value() def set_vertical_pos(self, position): """ Used to save the scrolled position and restore when needed """ self._v_vscrollbar.get_adjustment().set_value(position) def can_zoom_in(self): ''' Returns True if it is possible to zoom in further ''' if self.scale < 4: return True else: return False def can_zoom_out(self): ''' Returns True if it is possible to zoom out further ''' if self.scale > 0.5: return True else: return False def get_current_page(self): ''' Returns the currently loaded page ''' return self._loaded_page def get_current_file(self): ''' Returns the currently loaded XML file ''' #return self._loaded_filename if self._paginator: return self._paginator.get_file_for_pageno(self._loaded_page) else: return None def get_pagecount(self): ''' Returns the pagecount of the loaded file ''' return self._pagecount def set_current_page(self, n): ''' Loads page number n ''' if n < 1 or n > self._pagecount: return False self._load_page(n) return True def next_page(self): ''' Loads next page if possible Returns True if transition to next page is possible and done ''' if self._loaded_page == self._pagecount: return False self._load_next_page() return True def previous_page(self): ''' Loads previous page if possible Returns True if transition to previous page is possible and done ''' if self._loaded_page == 1: return False self._load_prev_page() return True def scroll(self, scrolltype, horizontal): ''' Scrolls through the pages. Scrolling is horizontal if horizontal is set to True Valid scrolltypes are: Gtk.ScrollType.PAGE_BACKWARD, Gtk.ScrollType.PAGE_FORWARD, Gtk.ScrollType.STEP_BACKWARD, Gtk.ScrollType.STEP_FORWARD Gtk.ScrollType.STEP_START and Gtk.ScrollType.STEP_STOP ''' if scrolltype == Gtk.ScrollType.PAGE_BACKWARD: self.__going_back = True self.__going_fwd = False if not self._do_page_transition(): self._view.move_cursor(Gtk.MovementStep.PAGES, -1) elif scrolltype == Gtk.ScrollType.PAGE_FORWARD: self.__going_back = False self.__going_fwd = True if not self._do_page_transition(): self._view.move_cursor(Gtk.MovementStep.PAGES, 1) elif scrolltype == Gtk.ScrollType.STEP_BACKWARD: self.__going_fwd = False self.__going_back = True if not self._do_page_transition(): self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, -1) elif scrolltype == Gtk.ScrollType.STEP_FORWARD: self.__going_fwd = True self.__going_back = False if not self._do_page_transition(): self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, 1) elif scrolltype == Gtk.ScrollType.START: self.__going_back = True self.__going_fwd = False if not self._do_page_transition(): self.set_current_page(1) elif scrolltype == Gtk.ScrollType.END: self.__going_back = False self.__going_fwd = True if not self._do_page_transition(): self.set_current_page(self._pagecount - 1) else: print ('Got unsupported scrolltype %s' % str(scrolltype)) def __touch_page_changed_cb(self, widget, forward): if forward: self.scroll(Gtk.ScrollType.PAGE_FORWARD, False) else: self.scroll(Gtk.ScrollType.PAGE_BACKWARD, False) def copy(self): ''' Copies the current selection to clipboard. ''' self._view.copy_clipboard() def find_next(self): ''' Highlights the next matching item for current search ''' self._view.grab_focus() if self._view.search_text(self._findjob.get_search_text(), self._findjob.get_case_sensitive(), True, False): return else: path = os.path.join(self._epub.get_basedir(), self._findjob.get_next_file()) self.__in_search = True self.__search_fwd = True self._load_file(path) def find_previous(self): ''' Highlights the previous matching item for current search ''' self._view.grab_focus() if self._view.search_text(self._findjob.get_search_text(), self._findjob.get_case_sensitive(), False, False): return else: path = os.path.join(self._epub.get_basedir(), self._findjob.get_prev_file()) self.__in_search = True self.__search_fwd = False self._load_file(path) def _find_changed(self, job): self._view.grab_focus() self._findjob = job self._mark_found_text() self.find_next() def _mark_found_text(self): self._view.unmark_text_matches() self._view.mark_text_matches(self._findjob.get_search_text(), case_sensitive=self._findjob.get_case_sensitive(), limit=0) self._view.set_highlight_text_matches(True) def __set_zoom(self, value): self._view.set_zoom_level(value) self.scale = value def _view_populate_popup_cb(self, view, menu): menu.destroy() # HACK return def _view_selection_changed_cb(self, view): self.emit('selection-changed') def _view_keypress_event_cb(self, view, event): name = Gdk.keyval_name(event.keyval) if name == 'Page_Down' or name == 'Down': self.__going_back = False self.__going_fwd = True elif name == 'Page_Up' or name == 'Up': self.__going_back = True self.__going_fwd = False self._do_page_transition() def _view_scroll_event_cb(self, view, event): if event.direction == Gdk.ScrollDirection.DOWN: self.__going_back = False self.__going_fwd = True elif event.direction == Gdk.ScrollDirection.UP: self.__going_back = True self.__going_fwd = False self._do_page_transition() def _do_page_transition(self): if self.__going_fwd: if self._v_vscrollbar.get_value() >= \ self._v_vscrollbar.props.adjustment.props.upper - \ self._v_vscrollbar.props.adjustment.props.page_size: self._load_page(self._loaded_page + 1) return True elif self.__going_back: if self._v_vscrollbar.get_value() == \ self._v_vscrollbar.props.adjustment.props.lower: self._load_page(self._loaded_page - 1) return True return False def _view_load_finished_cb(self, v, frame): self._file_loaded = True filename = self._view.props.uri.replace('file://', '') if os.path.exists(filename.replace('xhtml', 'xml')): # Hack for making javascript work filename = filename.replace('xhtml', 'xml') filename = filename.split('#')[0] # Get rid of anchors if self._loaded_page < 1 or filename == None: return False self._loaded_filename = filename remfactor = self._paginator.get_remfactor_for_file(filename) pages = self._paginator.get_pagecount_for_file(filename) extra = int(math.ceil(remfactor * self._view.get_page_height() / (pages - remfactor))) if extra > 0: self._view.add_bottom_padding(extra) if self.__in_search: self._mark_found_text() self._view.search_text(self._findjob.get_search_text(), \ self._findjob.get_case_sensitive(), \ self.__search_fwd, False) self.__in_search = False else: if self.__going_back: # We need to scroll to the last page self._scroll_page_end() else: self._scroll_page() process_file = True if self._internal_link is not None: self._view.go_to_link(self._internal_link) vertical_pos = \ self._view.get_vertical_position_element(self._internal_link) # set the page number based in the vertical position initial_page = self._paginator.get_base_pageno_for_file(filename) self._loaded_page = initial_page + int(vertical_pos / self._paginator.get_single_page_height()) # There are epub files, created with Calibre, # where the link in the index points to the end of the previos # file to the needed chapter. # if the link is at the bottom of the page, we open the next file one_page_height = self._paginator.get_single_page_height() self._internal_link = None if vertical_pos > self._view.get_page_height() - one_page_height: logging.error('bottom page link, go to next file') next_file = self._paginator.get_next_filename(filename) if next_file is not None: logging.error('load next file %s', next_file) self.__in_search = False self.__going_back = False process_file = False GObject.idle_add(self._load_file, next_file) # if process_file: # # prepare text to speech # html_file = open(self._loaded_filename) # soup = BeautifulSoup.BeautifulSoup(html_file) # body = soup.find('body') # tags = body.findAll(text=True) # self._all_text = ''.join([tag for tag in tags]) # self._prepare_text_to_speech(self._all_text) def _prepare_text_to_speech(self, page_text): i = 0 j = 0 word_begin = 0 word_end = 0 ignore_chars = [' ', '\n', u'\r', '_', '[', '{', ']', '}', '|', '<', '>', '*', '+', '/', '\\'] ignore_set = set(ignore_chars) self.word_tuples = [] len_page_text = len(page_text) while i < len_page_text: if page_text[i] not in ignore_set: word_begin = i j = i while j < len_page_text and page_text[j] not in ignore_set: j = j + 1 word_end = j i = j word_tuple = (word_begin, word_end, page_text[word_begin: word_end]) if word_tuple[2] != u'\r': self.word_tuples.append(word_tuple) i = i + 1 def _scroll_page_end(self): v_upper = self._v_vscrollbar.props.adjustment.props.upper v_page_size = self._v_vscrollbar.props.adjustment.props.page_size self._v_vscrollbar.set_value(v_upper) def _scroll_page(self): pageno = self._loaded_page v_upper = self._v_vscrollbar.props.adjustment.props.upper v_page_size = self._v_vscrollbar.props.adjustment.props.page_size scrollfactor = self._paginator.get_scrollfactor_pos_for_pageno(pageno) self._v_vscrollbar.set_value((v_upper - v_page_size) * scrollfactor) def _paginate(self): filelist = [] for i in self._epub._navmap.get_flattoc(): filelist.append(os.path.join(self._epub._tempdir, i)) # init files info self._filelist = filelist self._paginator = _Paginator(filelist) self._paginator.connect('paginated', self._paginated_cb) def get_filelist(self): return self._filelist def get_tempdir(self): return self._epub._tempdir def _load_next_page(self): self._load_page(self._loaded_page + 1) def _load_prev_page(self): self._load_page(self._loaded_page - 1) def _v_scrollbar_value_changed_cb(self, scrollbar): if self._loaded_page < 1: return scrollval = scrollbar.get_value() scroll_upper = self._v_vscrollbar.props.adjustment.props.upper scroll_page_size = self._v_vscrollbar.props.adjustment.props.page_size if self.__going_fwd == True and \ not self._loaded_page == self._pagecount: if self._paginator.get_file_for_pageno(self._loaded_page) != \ self._paginator.get_file_for_pageno(self._loaded_page + 1): # We don't need this if the next page is in another file return scrollfactor_next = \ self._paginator.get_scrollfactor_pos_for_pageno( self._loaded_page + 1) if scrollval > 0: scrollfactor = scrollval / (scroll_upper - scroll_page_size) else: scrollfactor = 0 if scrollfactor >= scrollfactor_next: self._on_page_changed(self._loaded_page, self._loaded_page + 1) elif self.__going_back == True and self._loaded_page > 1: if self._paginator.get_file_for_pageno(self._loaded_page) != \ self._paginator.get_file_for_pageno( self._loaded_page - 1): return scrollfactor_cur = \ self._paginator.get_scrollfactor_pos_for_pageno( self._loaded_page) if scrollval > 0: scrollfactor = scrollval / (scroll_upper - scroll_page_size) else: scrollfactor = 0 if scrollfactor <= scrollfactor_cur: self._on_page_changed(self._loaded_page, self._loaded_page - 1) def _on_page_changed(self, oldpage, pageno): if oldpage == pageno: return self.__page_changed = True self._loaded_page = pageno self._scrollbar.handler_block(self._scrollbar_change_value_cb_id) self._scrollbar.set_value(pageno) self._scrollbar.handler_unblock(self._scrollbar_change_value_cb_id) # the indexes in read activity are zero based self.emit('page-changed', (oldpage - 1), (pageno - 1)) def _load_page(self, pageno): if pageno > self._pagecount or pageno < 1: #TODO: Cause an exception return if self._loaded_page == pageno: return filename = self._paginator.get_file_for_pageno(pageno) filename = filename.replace('file://', '') if filename != self._loaded_filename: self._loaded_filename = filename if not self._file_loaded: # wait until the file is loaded return self._file_loaded = False """ TODO: disabled because javascript can't be executed with the velocity needed # Copy javascript to highligth text to speech destpath, destname = os.path.split(filename.replace('file://', '')) shutil.copy('./epubview/highlight_words.js', destpath) self._insert_js_reference(filename.replace('file://', ''), destpath) """ if filename.endswith('xml'): dest = filename.replace('xml', 'xhtml') if not os.path.exists(dest): os.symlink(filename, dest) self._view.load_uri('file://' + dest) else: self._view.load_uri('file://' + filename) else: self._loaded_page = pageno self._scroll_page() self._on_page_changed(self._loaded_page, pageno) def _insert_js_reference(self, file_name, path): js_reference = '' o = open(file_name + '.tmp', 'a') for line in open(file_name): line = line.replace('', js_reference + '') o.write(line + "\n") o.close() shutil.copy(file_name + '.tmp', file_name) def _load_file(self, path): self._internal_link = None if path.find('#') > -1: self._internal_link = path[path.find('#'):] path = path[:path.find('#')] for filepath in self._filelist: if filepath.endswith(path): self._view.load_uri('file://' + filepath) oldpage = self._loaded_page self._loaded_page = \ self._paginator.get_base_pageno_for_file(filepath) self._scroll_page() self._on_page_changed(oldpage, self._loaded_page) break def _scrollbar_change_value_cb(self, range, scrolltype, value): if scrolltype == Gtk.ScrollType.STEP_FORWARD: self.__going_fwd = True self.__going_back = False if not self._do_page_transition(): self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, 1) elif scrolltype == Gtk.ScrollType.STEP_BACKWARD: self.__going_fwd = False self.__going_back = True if not self._do_page_transition(): self._view.move_cursor(Gtk.MovementStep.DISPLAY_LINES, -1) elif scrolltype == Gtk.ScrollType.JUMP or \ scrolltype == Gtk.ScrollType.PAGE_FORWARD or \ scrolltype == Gtk.ScrollType.PAGE_BACKWARD: if value > self._scrollbar.props.adjustment.props.upper: self._load_page(self._pagecount) else: self._load_page(round(value)) else: print 'Warning: unknown scrolltype %s with value %f' \ % (str(scrolltype), value) #FIXME: This should not be needed here self._scrollbar.set_value(self._loaded_page) if self.__page_changed == True: self.__page_changed = False return False else: return True def _paginated_cb(self, object): self._ready = True self._pagecount = self._paginator.get_total_pagecount() self._scrollbar.set_range(1.0, self._pagecount - 1.0) self._scrollbar.set_increments(1.0, 1.0) self._view.grab_focus() self._view.grab_default() def _destroy_cb(self, widget): self._epub.close()