class LatexDocument(object): SAME_WIDGET = 1000 TEXT = 1001 TARGETS = [('SLIDEDEX_SLIDE', gtk.TARGET_SAME_WIDGET, SAME_WIDGET), ('text/plain', 0, TEXT)] def __init__(self, filename=None): builder = gtk.Builder() builder.add_from_file(os.path.join(LIBPATH, "mainwindow.glade")) self.get_objects(builder) vbox = builder.get_object("vbox1") hbox = builder.get_object("hbox1") menu, toolbar, slidebar = self.set_actions() vbox.pack_end(toolbar, False) vbox.pack_end(menu, False) hbox.pack_end(slidebar, False) self.viewer = PDFViewer(builder) builder.connect_signals(EventDispatcher(self)) self.settings = None self.header = HeaderFooter(self) self.footer = HeaderFooter(self) self.header_view = sourceview.View(self.header.buffer) gtkspell.Spell(self.header_view) builder.get_object("scrolledwindow1").add(self.header_view) self.currslide_view = sourceview.View() gtkspell.Spell(self.currslide_view) builder.get_object("scrolledwindow2").add(self.currslide_view) #self.empty_buffer = self.currslide_view.get_buffer() self.currslide_view.set_sensitive(False) self.footer_view = sourceview.View(self.footer.buffer) gtkspell.Spell(self.footer_view) builder.get_object("scrolledwindow3").add(self.footer_view) font_desc = pango.FontDescription('monospace') for view in (self.header_view, self.currslide_view, self.footer_view): view.set_wrap_mode(gtk.WRAP_WORD_CHAR) if font_desc: view.modify_font(font_desc) view.show() self.notebook.set_current_page(1) self.pages = gtk.ListStore(object, gtk.gdk.Pixbuf) self.pages.connect('row-inserted', self.on_row_inserted) self.pages.connect('row-deleted', self.on_row_deleted) self.pages.connect('rows-reordered', self.on_rows_reordered) self.slidelist_view.set_model(self.pages) self.slidelist_view.set_pixbuf_column(1) self.slidelist_view.set_columns(1000) self.slidelist_view.enable_model_drag_source(gtk.gdk.BUTTON1_MASK, self.TARGETS, gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY) self.slidelist_view.enable_model_drag_dest(self.TARGETS, gtk.gdk.ACTION_MOVE | gtk.gdk.ACTION_COPY) self.slidelist_view.connect('drag-data-get', self.on_drag_data_get) self.slidelist_view.connect('drag-data-received', self.on_drag_data_received) self._filename = None self._dir = None self.doc = None self._loaded = True self._order_modified = False if filename is not None: glib.idle_add(self.load, filename) self.executor = CommandExecutor(self) self.window.show() gtk.main() @property def dir(self): if not self._dir: self.save_as() if self._dir: return self._dir raise ObstinateUserError, "C'mon, we need a filename" @property def fullfilename(self): return os.path.join(self.dir, self._filename) @fullfilename.setter def fullfilename(self, filename): self._dir = os.path.dirname(os.path.abspath(filename)) self._filename = os.path.basename(filename) def load(self, filename): self.fullfilename = filename f = file(self.fullfilename, 'r') self._load(f) def _load(self, fobj): self._loaded = False if not fobj.readline().startswith(SEP): raise IOError, "Not a SlideDeX file" str = fobj.read() segments = str.split(SEP) if len(segments) < 3: raise IOError, "Could not load from file" self.settings = DocumentSettings(self, segments[0]) self.pages.clear() self.header.set_content(segments[1][1:]) # Ignore empty line for filename self.footer.set_content(segments[-1][1:]) for s in segments[2:-1]: filename, content = s.split('\n', 1) self.add_page(content, filename) pdffn = base_filename(self.fullfilename) + '.pdf' select_first_page = lambda status: self.slidelist_view.select_path((0,)) if os.path.exists(pdffn) and os.stat(pdffn).st_mtime >= os.stat(self.fullfilename).st_mtime: self.compile_pages() self.doc = poppler.document_new_from_file('file://' + os.path.abspath(pdffn), None) self.executor.add_callback(select_first_page) else: self.compile(select_first_page) self.modified = False # Set modified_since_save, and update the window title self._loaded = True def add_page(self, content="", filename="", after=None, before=None, render=False): slide = LatexSlide(self, content, filename, render=(render and content)) if after is not None: self.pages.insert_after(after, (slide, slide.pb)) elif before is not None: self.pages.insert_before(before, (slide, slide.pb)) else: self.pages.append((slide, slide.pb)) def delete_page(self, iter): slide, = self.pages.get(iter, 0) slide.del_files() self.pages.remove(iter) def _save(self, fobj): fobj.write(SEP + '\n') fobj.write(self.settings.write()) fobj.write(self.header.get_content()) for p in self.pages: fobj.write(p[0].get_content()) fobj.write(self.footer.get_content()) def save(self): f = file(self.fullfilename, 'w') self._save(f) f.close() self.modified = False @property def modified(self): if self._order_modified or self.header.modified_since_save \ or self.footer.modified_since_save: # Check these first since we set header as modified when the # slide ordering changes. In that case, all of the pages # may not have their LatexSlides yet, so we want to short- # circuit here. return True for p in self.pages: if p[0].modified_since_save: return True return False @modified.setter def modified(self, mod): self._order_modified = mod self.on_modified_changed() # We might not have a page that will call this. if mod: # We only need to set one to True return self.header.modified_since_save = mod self.footer.modified_since_save = mod for p in self.pages: p[0].modified_since_save = mod def on_modified_changed(self): name = self._filename or "Unnamed Presentation" if self.modified: self.window.set_title(name + '*') else: self.window.set_title(name) def compile_pages(self): for p,_ in self.pages: if p.modified_since_compile: def pagecallback(status, page=p): # Freeze p if status == 0: page.render_thumb() p.compile(pagecallback, False) def compile(self, callback=None, stop_on_error=True): self.compile_pages() if self.modified: self.save() self.do_latex(callback, stop_on_error) def do_latex(self, callback, stop_on_error): self._do_latex(self, self.settings.pres_command, callback, stop_on_error) def _do_latex(self, obj, command, callback, stop_on_error): # obj is either this LatexDocument or one of its LatexSlides fn = base_filename(obj.fullfilename) def after_latex(status): if status == 0: obj.doc = poppler.document_new_from_file('file://' + os.path.abspath(fn+'.pdf'), None) if callback: callback(status) commands = [[s.format(fn=fn) for s in c.split()] for c in command.split(';')] self.executor.add(commands, stop_on_error, (after_latex,)) def get_objects(self, builder): for object in ("window", "notebook", "view_slide_button", "view_presentation_button", "slidelist_view"): setattr(self, object, builder.get_object(object)) def set_actions(self): UI_STRING = """ <ui> <menubar name="TopMenu"> <menu action="file"> <menuitem action="new"/> <menuitem action="open"/> <menuitem action="save"/> <menuitem action="save-as"/> <separator/> <menuitem action="quit"/> </menu> <menu action="compile"> <menuitem action="compile-page"/> <menuitem action="compile-all"/> </menu> <menu action="slide" name="Slide"> <menuitem action="new-slide-menu" name="NewSlide"/> <menuitem action="delete-slide"/> <menuitem action="next-slide"/> <menuitem action="prev-slide"/> </menu> </menubar> <toolbar name="ToolBar" action="toolbar"> <placeholder name="JustifyToolItems"> <separator/> <toolitem action="new"/> <toolitem action="open"/> <separator/> <toolitem action="compile-page"/> <toolitem action="compile-all"/> <separator/> </placeholder> </toolbar> <toolbar name="SlideBar" action="toolbar"> <separator/> <toolitem action="new-slide-toolbar"/> <toolitem action="delete-slide"/> <toolitem action="prev-slide"/> <toolitem action="next-slide"/> <separator/> </toolbar> </ui>""" action_group = gtk.ActionGroup("main") action_group.add_actions([ ('file', None, "_File"), ('toolbar', None, "Huh?"), ('new', gtk.STOCK_NEW, "_New Presentation", "<control>n", None, self.on_new_pres), ('open', gtk.STOCK_OPEN, "_Open Presentation", "<control>o", None, self.on_open_pres), ('save', gtk.STOCK_SAVE, "_Save", "<control>s", None, self.on_save), ('save-as', gtk.STOCK_SAVE_AS, "Save _As", "<control><shift>s", None, self.save_as), ('quit', gtk.STOCK_QUIT, None, "<control>w", None, self.on_quit), ('compile', None, "_Compile"), ('compile-page', gtk.STOCK_CONVERT, "Compile Page", "<shift>Return", "Compile Page", self.on_compile_page), ('compile-all', gtk.STOCK_EXECUTE, "Compile Document", "<control><shift>Return", "Compile Document", self.on_compile_all), ('slide', None, "_Slide"), ('new-slide-menu', gtk.STOCK_NEW, "_New Slide", "", None, None), ('new-slide-toolbar', gtk.STOCK_NEW, "_New Slide", "", None, self.on_new_slide_toolbar), ('delete-slide', gtk.STOCK_DELETE, "_Delete Slide", "<shift>Delete", None, self.on_delete_slide), ('next-slide', gtk.STOCK_GO_FORWARD, "Next Slide", "Page_Down", None, self.on_next_slide), ('prev-slide', gtk.STOCK_GO_BACK, "Previous Slide", "Page_Up", None, self.on_prev_slide), ]) #action = action_group.get_action('new') #action.connect_proxy(newbutton) ui_manager = gtk.UIManager() ui_manager.insert_action_group(action_group) ui_manager.add_ui_from_string(UI_STRING) ui_manager.ensure_update() self._accel_group = ui_manager.get_accel_group() self.window.add_accel_group(self._accel_group) slidebar = ui_manager.get_widget("/SlideBar") slidebar.set_orientation(gtk.ORIENTATION_VERTICAL) slidebar.set_style(gtk.TOOLBAR_ICONS) slidebar.set_property('icon-size', gtk.ICON_SIZE_BUTTON) #MENU) self.skel_menu = gtk.Menu() ui_manager.get_widget("/TopMenu/Slide/NewSlide").set_submenu(self.skel_menu) return ui_manager.get_widget("/TopMenu"), ui_manager.get_widget("/ToolBar"), slidebar def generate_skeleton_menu(self): for item in self.skel_menu.get_children(): self.skel_menu.remove(item) first = True for key, val in self.settings.skeletons.items(): menuitem = gtk.MenuItem(key) menuitem.connect('activate', self.on_new_slide, val) menuitem.show() self.skel_menu.append(menuitem) if first: key, mod = gtk.accelerator_parse("<shift>Insert") menuitem.add_accelerator('activate', self._accel_group, key, mod, gtk.ACCEL_VISIBLE) first = False def on_window_delete(self, widget, event): if self.modified: dialog = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_NONE) dialog.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_QUIT, gtk.RESPONSE_NO, gtk.STOCK_SAVE, gtk.RESPONSE_YES) dialog.set_default_response(gtk.RESPONSE_YES) dialog.set_markup("<big><b>Unsaved Changes</b></big>\n\nThe presentation has unsaved changes.") dialog.show_all() response = dialog.run() dialog.destroy() if response in (gtk.RESPONSE_REJECT, gtk.RESPONSE_DELETE_EVENT): return True if response == gtk.RESPONSE_YES: try: self.save() except ObstinateUserError: return True return False def on_window_destroy(self, widget, data=None): # Delete temp files that (might) reflect unsaved changes docmtime = os.stat(self.fullfilename).st_mtime for p,_ in self.pages: p.del_files_mtime(docmtime) gtk.main_quit() def on_quit(self, action): if not self.on_window_delete(None, None): self.window.destroy() def on_save(self, action): self.save() def save_as(self, action=None): dialog = gtk.FileChooserDialog("Save As...", self.window, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) response = dialog.run() if response == gtk.RESPONSE_OK: self.fullfilename = dialog.get_filename() self.modified = True # To update window title self.save() dialog.destroy() def on_view_slide(self, widget): if widget.get_active(): selection = self.slidelist_view.get_selected_items() if selection: currslide, = self.pages.get(self.pages.get_iter(selection[0]), 0) self.viewer.load_doc(currslide.doc) else: # In odd cases, there can be a document, but no slides. self.viewer.load_doc(None) def on_view_presentation(self, widget): if widget.get_active(): self.viewer.load_doc(self.doc) def on_selection_changed(self, view): selection = self.slidelist_view.get_selected_items() if len(selection) == 0 and self.prev_selection and len(self.pages): if self.prev_selection[0] >= len(self.pages): self.prev_selection = (len(self.pages) - 1,) self.slidelist_view.select_path(self.prev_selection) elif len(selection) == 1: # Changing the buffer removes any selection, so we copy the # primary selection, if it exists. oldbuffer = self.currslide_view.get_buffer() if oldbuffer.get_has_selection(): oldbuffer.add_selection_clipboard(gtk.clipboard_get("PRIMARY")) currslide, = self.pages.get(self.pages.get_iter(selection[0]), 0) self.currslide_view.set_buffer(currslide.buffer) self.currslide_view.set_sensitive(True) self.prev_selection = selection[0] if self.view_slide_button.get_active(): self.viewer.load_doc(currslide.doc) else: self.currslide_view.set_sensitive(False) if len(self.pages) == 0 and self.view_slide_button.get_active(): self.viewer.load_doc(None) # Right now, these two are used by drag-and-drop reordering def on_row_inserted(self, model, path, iter): self.prev_selection = path if self._loaded: self.modified = True def on_row_deleted(self, model, path): if path[0] < self.prev_selection[0]: self.prev_selection = (self.prev_selection[0] - 1,) if self._loaded: self.modified = True def on_rows_reordered(self, model, path, iter, new_order): self.modified = True def on_new_slide_toolbar(self, action): self.skel_menu.popup(None, None, None, 1, gtk.get_current_event_time()) def on_new_slide(self, action, content=""): selection = self.slidelist_view.get_selected_items() if selection: selection = selection[0] iter = self.pages.get_iter(selection) else: iter = None self.add_page(content, after=iter) self.slidelist_view.unselect_all() if selection: self.slidelist_view.select_path((selection[0]+1,)) else: self.slidelist_view.select_path((0,)) # Switch to slide editor and focus self.notebook.set_current_page(1) self.currslide_view.grab_focus() def on_delete_slide(self, action): for selection in self.slidelist_view.get_selected_items(): iter = self.pages.get_iter(selection) self.delete_page(iter) # http://www.pygtk.org/pygtk2tutorial/sec-TreeViewDragAndDrop.html def on_drag_data_get(self, iconview, context, selection_data, target_id, etime): if target_id == self.SAME_WIDGET and context.action == gtk.gdk.ACTION_MOVE: self.drag_get_reorder(selection_data) else: self.drag_get_create(selection_data) def on_drag_data_received(self, iconview, context, x, y, selection_data, target_id, etime): drop_info = self.slidelist_view.get_dest_item_at_pos(x, y) if target_id == self.SAME_WIDGET and context.action == gtk.gdk.ACTION_MOVE: self.drag_received_reorder(context, drop_info, selection_data, etime) else: self.drag_received_create(context, drop_info, selection_data, etime) def drag_get_reorder(self, selection_data): data = repr(self.slidelist_view.get_selected_items()) selection_data.set(selection_data.target, 8, data) def drag_received_reorder(self, context, drop_info, selection_data, etime): data = eval(selection_data.data) # [(a,), (b,) ... data = [d[0] for d in data] # [a, b, ... data.sort() if drop_info: path, position = drop_info index = path[0] if position not in (gtk.ICON_VIEW_DROP_LEFT, gtk.ICON_VIEW_DROP_ABOVE): index += 1 else: index = len(self.pages) index0 = index order = range(len(self.pages)) for d in data: order.remove(d) if d < index0: index -= 1 assert(index >= 0) self.pages.reorder(order[:index] + data + order[index:]) def drag_get_create(self, selection_data): data = [] # Note that this puts the slides in reverse order. for selection in self.slidelist_view.get_selected_items(): slide, = self.pages.get(self.pages.get_iter(selection), 0) data.append(slide.get_content(raw=True)) selection_data.set(selection_data.target, 8, SEP.join(data)) def drag_received_create(self, context, drop_info, selection_data, etime): data = selection_data.data.split(SEP) self.prev_selection = None # Disable unselect preventer self.slidelist_view.unselect_all() if drop_info: path, position = drop_info iter = self.pages.get_iter(path) if position in (gtk.ICON_VIEW_DROP_LEFT, gtk.ICON_VIEW_DROP_ABOVE): data.reverse() # Put in forward order, since we'll insert them all newpath = path # before the same element. for d in data: self.add_page(d, before=iter, render=True) self.slidelist_view.select_path(newpath) newpath = (newpath[0] + 1,) # Scroll to the right if needed. (The left will be where we dropped it, # and therefore already in view self.slidelist_view.scroll_to_path((newpath[0] - 1,), False, 0, 0) else: newpath = (path[0] + 1,) # We want the data in reverse order, since we'll be inserting them # all after the same element. for d in data: self.add_page(d, after=iter, render=True) self.slidelist_view.select_path(newpath) self.slidelist_view.scroll_to_path((path[0] + len(data),), False, 0, 0) else: for d in data: self.add_page(d, render=True) self.slidelist_view.select_path((len(self.pages) - 1,)) self.slidelist_view.scroll_to_path((len(self.pages)-1,), False, 0, 0) # Run on_selection_changed when we're done rendering all the new pages, so # we can load the current document into the viewer. This doesn't work if # we have multiple pages chosen, but the page view is sorta odd in that # case, regardless. self.executor.add_callback(self.on_selection_changed) if context.action == gtk.gdk.ACTION_MOVE: context.finish(True, True, etime) def on_compile_page(self, action): selection = self.slidelist_view.get_selected_items() if len(selection) == 1: currslide, = self.pages.get(self.pages.get_iter(selection[0]), 0) def callback(status): if status == 0: currslide.render_thumb() self.view_slide_button.clicked() currslide.compile(callback) def on_compile_all(self, action): self.compile(lambda status: not status and self.view_presentation_button.clicked()) def on_next_slide(self, action): selection = self.slidelist_view.get_selected_items() if selection: selection = selection[0][0] if selection < len(self.pages)-1: self.prev_selection = None self.slidelist_view.unselect_all() self.slidelist_view.select_path((selection+1,)) self.slidelist_view.scroll_to_path((selection+1,), False, 0, 0) def on_prev_slide(self, action): selection = self.slidelist_view.get_selected_items() if selection: selection = selection[-1][0] if selection > 0: self.prev_selection = None self.slidelist_view.unselect_all() self.slidelist_view.select_path((selection-1,)) self.slidelist_view.scroll_to_path((selection-1,), False, 0, 0) def on_new_pres(self, action): LatexDocument() def on_open_pres(self, action): dialog = gtk.FileChooserDialog("Open...", self.window, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)) response = dialog.run() if response == gtk.RESPONSE_OK: filename = dialog.get_filename() if not self._filename and not self.modified: glib.idle_add(self.load, filename) else: glib.idle_add(lambda: LatexDocument(filename) and False) dialog.destroy()