From: Mathieu Baudier Date: Fri, 20 Apr 2018 10:57:50 +0000 (+0200) Subject: Start implementing DocBook editor X-Git-Tag: argeo-commons-2.1.74~51 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=a0fd509bd07db921be334906ccd69edf81708ff0 Start implementing DocBook editor --- diff --git a/demo/argeo_node_cms.properties b/demo/argeo_node_cms.properties index 75e79917c..a11d723dd 100644 --- a/demo/argeo_node_cms.properties +++ b/demo/argeo_node_cms.properties @@ -1,8 +1,9 @@ -argeo.osgi.start.2.node=\ +argeo.osgi.start.2.http=\ org.eclipse.equinox.http.servlet,\ org.eclipse.equinox.http.jetty,\ -org.eclipse.equinox.cm,\ +org.eclipse.equinox.metatype,\ org.eclipse.rap.rwt.osgi,\ +org.eclipse.equinox.cm argeo.osgi.start.3.node=\ org.argeo.cms,\ diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/DbkTextInterpreter.java b/org.argeo.cms.ui/src/org/argeo/cms/text/DbkTextInterpreter.java new file mode 100644 index 000000000..aa32b3bb4 --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/text/DbkTextInterpreter.java @@ -0,0 +1,96 @@ +package org.argeo.cms.text; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.argeo.cms.CmsException; +import org.argeo.jcr.docbook.DocBookNames; +import org.argeo.jcr.docbook.DocBookTypes; + +/** Based on HTML with a few Wiki-like shortcuts. */ +public class DbkTextInterpreter implements TextInterpreter { + + @Override + public void write(Item item, String content) { + try { + if (item instanceof Node) { + Node node = (Node) item; + if (node.isNodeType(DocBookTypes.PARA)) { + String raw = convertToStorage(node, content); + validateBeforeStoring(raw); + Node jcrText; + if (!node.hasNode(DocBookNames.JCR_XMLTEXT)) + jcrText = node.addNode(DocBookNames.JCR_XMLTEXT, DocBookTypes.XMLTEXT); + else + jcrText = node.getNode(DocBookNames.JCR_XMLTEXT); + jcrText.setProperty(DocBookNames.JCR_XMLCHARACTERS, raw); + } else { + throw new CmsException("Don't know how to interpret " + node); + } + } else {// property + Property property = (Property) item; + property.setValue(content); + } + // item.getSession().save(); + } catch (RepositoryException e) { + throw new CmsException("Cannot set content on " + item, e); + } + } + + @Override + public String read(Item item) { + try { + String raw = raw(item); + return convertFromStorage(item, raw); + } catch (RepositoryException e) { + throw new CmsException("Cannot get " + item + " for edit", e); + } + } + + @Override + public String raw(Item item) { + try { + item.getSession().refresh(true); + if (item instanceof Node) { + Node node = (Node) item; + if (node.isNodeType(DocBookTypes.PARA)) { + // WORKAROUND FOR BROKEN PARARAPHS + // if (!node.hasProperty(CMS_CONTENT)) { + // node.setProperty(CMS_CONTENT, ""); + // node.getSession().save(); + // } + Node jcrText = node.getNode(DocBookNames.JCR_XMLTEXT); + return jcrText.getProperty(DocBookNames.JCR_XMLCHARACTERS).getString(); + } else { + throw new CmsException("Don't know how to interpret " + node); + } + } else {// property + Property property = (Property) item; + return property.getString(); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot get " + item + " content", e); + } + } + + // EXTENSIBILITY + /** + * To be overridden, in order to make sure that only valid strings are being + * stored. + */ + protected void validateBeforeStoring(String raw) { + } + + /** To be overridden, in order to support additional formatting. */ + protected String convertToStorage(Item item, String content) throws RepositoryException { + return content; + + } + + /** To be overridden, in order to support additional formatting. */ + protected String convertFromStorage(Item item, String content) throws RepositoryException { + return content; + } +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentPage.java b/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentPage.java new file mode 100644 index 000000000..28436ca0a --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentPage.java @@ -0,0 +1,61 @@ +package org.argeo.cms.text; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.cms.CmsNames; +import org.argeo.cms.ui.CmsEditable; +import org.argeo.cms.ui.CmsUiProvider; +import org.argeo.cms.util.CmsLink; +import org.argeo.cms.util.CmsUtils; +import org.argeo.cms.viewers.JcrVersionCmsEditable; +import org.argeo.cms.widgets.ScrolledPage; +import org.argeo.jcr.JcrUtils; +import org.argeo.jcr.docbook.DocBookTypes; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** + * Display the text of the context, and provide an editor if the user can edit. + */ +public class DocumentPage implements CmsUiProvider { + @Override + public Control createUi(Composite parent, Node context) throws RepositoryException { + CmsEditable cmsEditable = new JcrVersionCmsEditable(context); + if (cmsEditable.canEdit()) + new TextEditorHeader(cmsEditable, parent, SWT.NONE).setLayoutData(CmsUtils.fillWidth()); + + ScrolledPage page = new ScrolledPage(parent, SWT.NONE); + page.setLayout(CmsUtils.noSpaceGridLayout()); + GridData textGd = CmsUtils.fillAll(); + page.setLayoutData(textGd); + + if (context.isNodeType(DocBookTypes.ARTICLE)) { + new DocumentTextEditor(page, SWT.NONE, context, cmsEditable); + } else if (context.isNodeType(NodeType.NT_FOLDER) || context.getPath().equals("/")) { + parent.setBackgroundMode(SWT.INHERIT_NONE); + if (context.getSession().hasPermission(context.getPath(), Session.ACTION_ADD_NODE)) { + Node indexNode = JcrUtils.getOrAdd(context, CmsNames.CMS_INDEX, DocBookTypes.ARTICLE); + new DocumentTextEditor(page, SWT.NONE, indexNode, cmsEditable); + textGd.heightHint = 400; + + for (NodeIterator ni = context.getNodes(); ni.hasNext();) { + Node textNode = ni.nextNode(); + if (textNode.isNodeType(NodeType.NT_FOLDER)) + new CmsLink(textNode.getName() + "/", textNode.getPath()).createUi(parent, textNode); + } + for (NodeIterator ni = context.getNodes(); ni.hasNext();) { + Node textNode = ni.nextNode(); + if (textNode.isNodeType(DocBookTypes.ARTICLE) && !textNode.getName().equals(CmsNames.CMS_INDEX)) + new CmsLink(textNode.getName(), textNode.getPath()).createUi(parent, textNode); + } + } + } + return page; + } +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentTextEditor.java b/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentTextEditor.java new file mode 100644 index 000000000..1b1e5c43c --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentTextEditor.java @@ -0,0 +1,41 @@ +package org.argeo.cms.text; + +import static javax.jcr.Property.JCR_TITLE; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.argeo.cms.ui.CmsEditable; +import org.argeo.cms.ui.internal.text.AbstractDbkViewer; +import org.argeo.cms.util.CmsUtils; +import org.argeo.jcr.docbook.DocBookNames; +import org.argeo.jcr.docbook.DocBookTypes; +import org.eclipse.swt.widgets.Composite; + +/** Text editor where sections and subsections can be managed by the user. */ +public class DocumentTextEditor extends AbstractDbkViewer { + private static final long serialVersionUID = 6049661610883342325L; + + public DocumentTextEditor(Composite parent, int style, Node textNode, CmsEditable cmsEditable) + throws RepositoryException { + super(new TextSection(parent, style, textNode), style, cmsEditable); + refresh(); + getMainSection().setLayoutData(CmsUtils.fillWidth()); + } + + @Override + protected void initModel(Node textNode) throws RepositoryException { + if (isFlat()) + textNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA); + else + textNode.setProperty(JCR_TITLE, textNode.getName()); + } + + @Override + protected Boolean isModelInitialized(Node textNode) throws RepositoryException { + return textNode.hasProperty(Property.JCR_TITLE) || textNode.hasNode(DocBookNames.DBK_PARA) + || (!isFlat() && textNode.hasNode(DocBookNames.DBK_SECTION)); + } + +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractDbkViewer.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractDbkViewer.java new file mode 100644 index 000000000..32c51adf5 --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractDbkViewer.java @@ -0,0 +1,836 @@ +package org.argeo.cms.ui.internal.text; + +import static javax.jcr.Property.JCR_TITLE; +import static org.argeo.cms.util.CmsUtils.fillWidth; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Observer; + +import javax.jcr.Item; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.CmsException; +import org.argeo.cms.text.DbkTextInterpreter; +import org.argeo.cms.text.Img; +import org.argeo.cms.text.Paragraph; +import org.argeo.cms.text.TextInterpreter; +import org.argeo.cms.text.TextSection; +import org.argeo.cms.ui.CmsEditable; +import org.argeo.cms.ui.CmsImageManager; +import org.argeo.cms.util.CmsUtils; +import org.argeo.cms.viewers.AbstractPageViewer; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.viewers.NodePart; +import org.argeo.cms.viewers.PropertyPart; +import org.argeo.cms.viewers.Section; +import org.argeo.cms.viewers.SectionPart; +import org.argeo.cms.widgets.EditableImage; +import org.argeo.cms.widgets.EditableText; +import org.argeo.cms.widgets.StyledControl; +import org.argeo.jcr.JcrUtils; +import org.argeo.jcr.docbook.DocBookNames; +import org.argeo.jcr.docbook.DocBookTypes; +import org.eclipse.rap.fileupload.FileDetails; +import org.eclipse.rap.fileupload.FileUploadEvent; +import org.eclipse.rap.fileupload.FileUploadHandler; +import org.eclipse.rap.fileupload.FileUploadListener; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Text; + +/** Base class for text viewers and editors. */ +public abstract class AbstractDbkViewer extends AbstractPageViewer implements KeyListener, Observer { + private static final long serialVersionUID = -2401274679492339668L; + private final static Log log = LogFactory.getLog(AbstractDbkViewer.class); + + private final Section mainSection; + + private TextInterpreter textInterpreter = new DbkTextInterpreter(); + private CmsImageManager imageManager = CmsUtils.getCmsView().getImageManager(); + + private FileUploadListener fileUploadListener; + private DbkContextMenu styledTools; + + private final boolean flat; + + protected AbstractDbkViewer(Section parent, int style, CmsEditable cmsEditable) { + super(parent, style, cmsEditable); + flat = SWT.FLAT == (style & SWT.FLAT); + + if (getCmsEditable().canEdit()) { + fileUploadListener = new FUL(); + styledTools = new DbkContextMenu(this, parent.getDisplay()); + } + this.mainSection = parent; + initModelIfNeeded(mainSection.getNode()); + // layout(this.mainSection); + } + + @Override + public Control getControl() { + return mainSection; + } + + protected void refresh(Control control) throws RepositoryException { + if (!(control instanceof Section)) + return; + Section section = (Section) control; + if (section instanceof TextSection) { + CmsUtils.clear(section); + Node node = section.getNode(); + TextSection textSection = (TextSection) section; + if (node.hasProperty(Property.JCR_TITLE)) { + if (section.getHeader() == null) + section.createHeader(); + if (node.hasProperty(Property.JCR_TITLE)) { + SectionTitle title = newSectionTitle(textSection, node); + title.setLayoutData(CmsUtils.fillWidth()); + updateContent(title); + } + } + + for (NodeIterator ni = node.getNodes(DocBookNames.DBK_PARA); ni.hasNext();) { + Node child = ni.nextNode(); + final SectionPart sectionPart; + if (child.isNodeType(DocBookTypes.IMAGEDATA) || child.isNodeType(NodeType.NT_FILE)) { + // FIXME adapt to DocBook + sectionPart = newImg(textSection, child); + } else if (child.isNodeType(DocBookTypes.PARA)) { + sectionPart = newParagraph(textSection, child); + } else { + sectionPart = newSectionPart(textSection, child); + if (sectionPart == null) + throw new CmsException("Unsupported node " + child); + // TODO list node types in exception + } + if (sectionPart instanceof Control) + ((Control) sectionPart).setLayoutData(CmsUtils.fillWidth()); + } + + if (!flat) + for (NodeIterator ni = section.getNode().getNodes(DocBookNames.DBK_SECTION); ni.hasNext();) { + Node child = ni.nextNode(); + if (child.isNodeType(DocBookTypes.SECTION)) { + TextSection newSection = new TextSection(section, SWT.NONE, child); + newSection.setLayoutData(CmsUtils.fillWidth()); + refresh(newSection); + } + } + } else { + for (Section s : section.getSubSections().values()) + refresh(s); + } + // section.layout(); + } + + /** To be overridden in order to provide additional SectionPart types */ + protected SectionPart newSectionPart(TextSection textSection, Node node) { + return null; + } + + // CRUD + protected Paragraph newParagraph(TextSection parent, Node node) throws RepositoryException { + Paragraph paragraph = new Paragraph(parent, parent.getStyle(), node); + updateContent(paragraph); + paragraph.setLayoutData(fillWidth()); + paragraph.setMouseListener(getMouseListener()); + return paragraph; + } + + protected Img newImg(TextSection parent, Node node) throws RepositoryException { + Img img = new Img(parent, parent.getStyle(), node) { + private static final long serialVersionUID = 1297900641952417540L; + + @Override + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + } + + @Override + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + } + }; + img.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + updateContent(img); + img.setMouseListener(getMouseListener()); + return img; + } + + protected SectionTitle newSectionTitle(TextSection parent, Node node) throws RepositoryException { + SectionTitle title = new SectionTitle(parent.getHeader(), parent.getStyle(), node.getProperty(JCR_TITLE)); + updateContent(title); + title.setMouseListener(getMouseListener()); + return title; + } + + protected SectionTitle prepareSectionTitle(Section newSection, String titleText) throws RepositoryException { + Node sectionNode = newSection.getNode(); + if (!sectionNode.hasProperty(JCR_TITLE)) + sectionNode.setProperty(Property.JCR_TITLE, ""); + getTextInterpreter().write(sectionNode.getProperty(Property.JCR_TITLE), titleText); + if (newSection.getHeader() == null) + newSection.createHeader(); + SectionTitle sectionTitle = newSectionTitle((TextSection) newSection, sectionNode); + return sectionTitle; + } + + protected void updateContent(EditablePart part) throws RepositoryException { + if (part instanceof SectionPart) { + SectionPart sectionPart = (SectionPart) part; + Node partNode = sectionPart.getNode(); + + if (part instanceof StyledControl && (sectionPart.getSection() instanceof TextSection)) { + TextSection section = (TextSection) sectionPart.getSection(); + StyledControl styledControl = (StyledControl) part; + if (partNode.isNodeType(DocBookTypes.PARA)) { + String style = partNode.hasProperty(DocBookNames.DBK_ROLE) + ? partNode.getProperty(DocBookNames.DBK_ROLE).getString() + : section.getDefaultTextStyle(); + styledControl.setStyle(style); + } + } + // use control AFTER setting style, since it may have been reset + + if (part instanceof EditableText) { + EditableText paragraph = (EditableText) part; + if (paragraph == getEdited()) + paragraph.setText(textInterpreter.read(partNode)); + else + paragraph.setText(textInterpreter.raw(partNode)); + } else if (part instanceof EditableImage) { + EditableImage editableImage = (EditableImage) part; + imageManager.load(partNode, part.getControl(), editableImage.getPreferredImageSize()); + } + } else if (part instanceof SectionTitle) { + SectionTitle title = (SectionTitle) part; + title.setStyle(title.getSection().getTitleStyle()); + // use control AFTER setting style + if (title == getEdited()) + title.setText(textInterpreter.read(title.getProperty())); + else + title.setText(textInterpreter.raw(title.getProperty())); + } + } + + // OVERRIDDEN FROM PARENT VIEWER + @Override + protected void save(EditablePart part) throws RepositoryException { + if (part instanceof EditableText) { + EditableText et = (EditableText) part; + String text = ((Text) et.getControl()).getText(); + + String[] lines = text.split("[\r\n]+"); + assert lines.length != 0; + saveLine(part, lines[0]); + if (lines.length > 1) { + ArrayList toLayout = new ArrayList(); + if (part instanceof Paragraph) { + Paragraph currentParagraph = (Paragraph) et; + Section section = currentParagraph.getSection(); + Node sectionNode = section.getNode(); + Node currentParagraphN = currentParagraph.getNode(); + for (int i = 1; i < lines.length; i++) { + Node newNode = sectionNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA); + // newNode.addMixin(CmsTypes.CMS_STYLED); + saveLine(newNode, lines[i]); + // second node was create as last, if it is not the next + // one, it + // means there are some in between and we can take the + // one at + // index+1 for the re-order + if (newNode.getIndex() > currentParagraphN.getIndex() + 1) { + sectionNode.orderBefore(p(newNode.getIndex()), p(currentParagraphN.getIndex() + 1)); + } + Paragraph newParagraph = newParagraph((TextSection) section, newNode); + newParagraph.moveBelow(currentParagraph); + toLayout.add(newParagraph); + + currentParagraph = newParagraph; + currentParagraphN = newNode; + } + persistChanges(sectionNode); + } + // TODO or rather return the created paragarphs? + layout(toLayout.toArray(new Control[toLayout.size()])); + } + } + } + + protected void saveLine(EditablePart part, String line) { + if (part instanceof NodePart) { + saveLine(((NodePart) part).getNode(), line); + } else if (part instanceof PropertyPart) { + saveLine(((PropertyPart) part).getProperty(), line); + } else { + throw new CmsException("Unsupported part " + part); + } + } + + protected void saveLine(Item item, String line) { + line = line.trim(); + textInterpreter.write(item, line); + } + + @Override + protected void prepare(EditablePart part, Object caretPosition) { + Control control = part.getControl(); + if (control instanceof Text) { + Text text = (Text) control; + if (caretPosition != null) + if (caretPosition instanceof Integer) + text.setSelection((Integer) caretPosition); + else if (caretPosition instanceof Point) { + // TODO find a way to position the caret at the right place + } + text.setData(RWT.ACTIVE_KEYS, new String[] { "BACKSPACE", "ESC", "TAB", "SHIFT+TAB", "ALT+ARROW_LEFT", + "ALT+ARROW_RIGHT", "ALT+ARROW_UP", "ALT+ARROW_DOWN", "RETURN", "CTRL+RETURN", "ENTER", "DELETE" }); + text.setData(RWT.CANCEL_KEYS, new String[] { "RETURN", "ALT+ARROW_LEFT", "ALT+ARROW_RIGHT" }); + text.addKeyListener(this); + } else if (part instanceof Img) { + ((Img) part).setFileUploadListener(fileUploadListener); + } + } + + // REQUIRED BY CONTEXT MENU + void setParagraphStyle(Paragraph paragraph, String style) { + try { + Node paragraphNode = paragraph.getNode(); + paragraphNode.setProperty(DocBookNames.DBK_ROLE, style); + persistChanges(paragraphNode); + updateContent(paragraph); + layout(paragraph); + } catch (RepositoryException e1) { + throw new CmsException("Cannot set style " + style + " on " + paragraph, e1); + } + } + + void deletePart(SectionPart paragraph) { + try { + Node paragraphNode = paragraph.getNode(); + Section section = paragraph.getSection(); + Session session = paragraphNode.getSession(); + paragraphNode.remove(); + session.save(); + if (paragraph instanceof Control) + ((Control) paragraph).dispose(); + layout(section); + } catch (RepositoryException e1) { + throw new CmsException("Cannot delete " + paragraph, e1); + } + } + + String getRawParagraphText(Paragraph paragraph) { + return textInterpreter.raw(paragraph.getNode()); + } + + // COMMANDS + protected void splitEdit() { + checkEdited(); + try { + if (getEdited() instanceof Paragraph) { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + int caretPosition = text.getCaretPosition(); + String txt = text.getText(); + String first = txt.substring(0, caretPosition); + String second = txt.substring(caretPosition); + Node firstNode = paragraph.getNode(); + Node sectionNode = firstNode.getParent(); + + // FIXME set content the DocBook way + // firstNode.setProperty(CMS_CONTENT, first); + Node secondNode = sectionNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA); + // secondNode.addMixin(CmsTypes.CMS_STYLED); + + // second node was create as last, if it is not the next one, it + // means there are some in between and we can take the one at + // index+1 for the re-order + if (secondNode.getIndex() > firstNode.getIndex() + 1) { + sectionNode.orderBefore(p(secondNode.getIndex()), p(firstNode.getIndex() + 1)); + } + + // if we die in between, at least we still have the whole text + // in the first node + try { + textInterpreter.write(secondNode, second); + textInterpreter.write(firstNode, first); + } catch (Exception e) { + // so that no additional nodes are created: + JcrUtils.discardUnderlyingSessionQuietly(firstNode); + throw e; + } + + persistChanges(firstNode); + + Paragraph secondParagraph = paragraphSplitted(paragraph, secondNode); + edit(secondParagraph, 0); + } else if (getEdited() instanceof SectionTitle) { + SectionTitle sectionTitle = (SectionTitle) getEdited(); + Text text = (Text) sectionTitle.getControl(); + String txt = text.getText(); + int caretPosition = text.getCaretPosition(); + Section section = sectionTitle.getSection(); + Node sectionNode = section.getNode(); + Node paragraphNode = sectionNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA); + // paragraphNode.addMixin(CmsTypes.CMS_STYLED); + + textInterpreter.write(paragraphNode, txt.substring(caretPosition)); + textInterpreter.write(sectionNode.getProperty(Property.JCR_TITLE), txt.substring(0, caretPosition)); + sectionNode.orderBefore(p(paragraphNode.getIndex()), p(1)); + persistChanges(sectionNode); + + Paragraph paragraph = sectionTitleSplitted(sectionTitle, paragraphNode); + // section.layout(); + edit(paragraph, 0); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot split " + getEdited(), e); + } + } + + protected void mergeWithPrevious() { + checkEdited(); + try { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + String txt = text.getText(); + Node paragraphNode = paragraph.getNode(); + if (paragraphNode.getIndex() == 1) + return;// do nothing + Node sectionNode = paragraphNode.getParent(); + Node previousNode = sectionNode.getNode(p(paragraphNode.getIndex() - 1)); + String previousTxt = textInterpreter.read(previousNode); + textInterpreter.write(previousNode, previousTxt + txt); + paragraphNode.remove(); + persistChanges(sectionNode); + + Paragraph previousParagraph = paragraphMergedWithPrevious(paragraph, previousNode); + edit(previousParagraph, previousTxt.length()); + } catch (RepositoryException e) { + throw new CmsException("Cannot stop editing", e); + } + } + + protected void mergeWithNext() { + checkEdited(); + try { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + String txt = text.getText(); + Node paragraphNode = paragraph.getNode(); + Node sectionNode = paragraphNode.getParent(); + NodeIterator paragraphNodes = sectionNode.getNodes(DocBookNames.DBK_PARA); + long size = paragraphNodes.getSize(); + if (paragraphNode.getIndex() == size) + return;// do nothing + Node nextNode = sectionNode.getNode(p(paragraphNode.getIndex() + 1)); + String nextTxt = textInterpreter.read(nextNode); + textInterpreter.write(paragraphNode, txt + nextTxt); + + Section section = paragraph.getSection(); + Paragraph removed = (Paragraph) section.getSectionPart(nextNode.getIdentifier()); + + nextNode.remove(); + persistChanges(sectionNode); + + paragraphMergedWithNext(paragraph, removed); + edit(paragraph, txt.length()); + } catch (RepositoryException e) { + throw new CmsException("Cannot stop editing", e); + } + } + + protected synchronized void upload(EditablePart part) { + try { + if (part instanceof SectionPart) { + SectionPart sectionPart = (SectionPart) part; + Node partNode = sectionPart.getNode(); + int partIndex = partNode.getIndex(); + Section section = sectionPart.getSection(); + Node sectionNode = section.getNode(); + + if (part instanceof Paragraph) { + // FIXME adapt to DocBook + Node newNode = sectionNode.addNode(DocBookNames.DBK_MEDIAOBJECT, NodeType.NT_FILE); + newNode.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE); + JcrUtils.copyBytesAsFile(sectionNode, p(newNode.getIndex()), new byte[0]); + if (partIndex < newNode.getIndex() - 1) { + // was not last + sectionNode.orderBefore(p(newNode.getIndex()), p(partIndex - 1)); + } + // sectionNode.orderBefore(p(partNode.getIndex()), + // p(newNode.getIndex())); + persistChanges(sectionNode); + Img img = newImg((TextSection) section, newNode); + edit(img, null); + layout(img.getControl()); + } else if (part instanceof Img) { + if (getEdited() == part) + return; + edit(part, null); + layout(part.getControl()); + } + } + } catch (RepositoryException e) { + throw new CmsException("Cannot upload", e); + } + } + + protected void deepen() { + if (flat) + return; + checkEdited(); + try { + if (getEdited() instanceof Paragraph) { + Paragraph paragraph = (Paragraph) getEdited(); + Text text = (Text) paragraph.getControl(); + String txt = text.getText(); + Node paragraphNode = paragraph.getNode(); + Section section = paragraph.getSection(); + Node sectionNode = section.getNode(); + // main title + if (section == mainSection && section instanceof TextSection && paragraphNode.getIndex() == 1 + && !sectionNode.hasProperty(JCR_TITLE)) { + SectionTitle sectionTitle = prepareSectionTitle(section, txt); + edit(sectionTitle, 0); + return; + } + Node newSectionNode = sectionNode.addNode(DocBookNames.DBK_SECTION, DocBookTypes.SECTION); + sectionNode.orderBefore(h(newSectionNode.getIndex()), h(1)); + + int paragraphIndex = paragraphNode.getIndex(); + String sectionPath = sectionNode.getPath(); + String newSectionPath = newSectionNode.getPath(); + while (sectionNode.hasNode(p(paragraphIndex + 1))) { + Node parag = sectionNode.getNode(p(paragraphIndex + 1)); + sectionNode.getSession().move(sectionPath + '/' + p(paragraphIndex + 1), + newSectionPath + '/' + DocBookNames.DBK_PARA); + SectionPart sp = section.getSectionPart(parag.getIdentifier()); + if (sp instanceof Control) + ((Control) sp).dispose(); + } + // create property + newSectionNode.setProperty(Property.JCR_TITLE, ""); + getTextInterpreter().write(newSectionNode.getProperty(Property.JCR_TITLE), txt); + + TextSection newSection = new TextSection(section, section.getStyle(), newSectionNode); + newSection.setLayoutData(CmsUtils.fillWidth()); + newSection.moveBelow(paragraph); + + // dispose + paragraphNode.remove(); + paragraph.dispose(); + + refresh(newSection); + newSection.getParent().layout(); + layout(newSection); + persistChanges(sectionNode); + } else if (getEdited() instanceof SectionTitle) { + SectionTitle sectionTitle = (SectionTitle) getEdited(); + Section section = sectionTitle.getSection(); + Section parentSection = section.getParentSection(); + if (parentSection == null) + return;// cannot deepen main section + Node sectionN = section.getNode(); + Node parentSectionN = parentSection.getNode(); + if (sectionN.getIndex() == 1) + return;// cannot deepen first section + Node previousSectionN = parentSectionN.getNode(h(sectionN.getIndex() - 1)); + NodeIterator subSections = previousSectionN.getNodes(DocBookNames.DBK_SECTION); + int subsectionsCount = (int) subSections.getSize(); + previousSectionN.getSession().move(sectionN.getPath(), + previousSectionN.getPath() + "/" + h(subsectionsCount + 1)); + section.dispose(); + TextSection newSection = new TextSection(section, section.getStyle(), sectionN); + refresh(newSection); + persistChanges(previousSectionN); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot deepen " + getEdited(), e); + } + } + + protected void undeepen() { + if (flat) + return; + checkEdited(); + try { + if (getEdited() instanceof Paragraph) { + upload(getEdited()); + } else if (getEdited() instanceof SectionTitle) { + SectionTitle sectionTitle = (SectionTitle) getEdited(); + Section section = sectionTitle.getSection(); + Node sectionNode = section.getNode(); + Section parentSection = section.getParentSection(); + if (parentSection == null) + return;// cannot undeepen main section + + // choose in which section to merge + Section mergedSection; + if (sectionNode.getIndex() == 1) + mergedSection = section.getParentSection(); + else { + Map parentSubsections = parentSection.getSubSections(); + ArrayList
lst = new ArrayList
(parentSubsections.values()); + mergedSection = lst.get(sectionNode.getIndex() - 1); + } + Node mergedNode = mergedSection.getNode(); + boolean mergedHasSubSections = mergedNode.hasNode(DocBookNames.DBK_SECTION); + + // title as paragraph + Node newParagrapheNode = mergedNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA); + // newParagrapheNode.addMixin(CmsTypes.CMS_STYLED); + if (mergedHasSubSections) + mergedNode.orderBefore(p(newParagrapheNode.getIndex()), h(1)); + String txt = getTextInterpreter().read(sectionNode.getProperty(Property.JCR_TITLE)); + getTextInterpreter().write(newParagrapheNode, txt); + // move + NodeIterator paragraphs = sectionNode.getNodes(DocBookNames.DBK_PARA); + while (paragraphs.hasNext()) { + Node p = paragraphs.nextNode(); + SectionPart sp = section.getSectionPart(p.getIdentifier()); + if (sp instanceof Control) + ((Control) sp).dispose(); + mergedNode.getSession().move(p.getPath(), mergedNode.getPath() + '/' + DocBookNames.DBK_PARA); + if (mergedHasSubSections) + mergedNode.orderBefore(p(p.getIndex()), h(1)); + } + + Iterator
subsections = section.getSubSections().values().iterator(); + // NodeIterator sections = sectionNode.getNodes(CMS_H); + while (subsections.hasNext()) { + Section subsection = subsections.next(); + Node s = subsection.getNode(); + mergedNode.getSession().move(s.getPath(), mergedNode.getPath() + '/' + DocBookNames.DBK_SECTION); + subsection.dispose(); + } + + // remove section + section.getNode().remove(); + section.dispose(); + + refresh(mergedSection); + mergedSection.getParent().layout(); + layout(mergedSection); + persistChanges(mergedNode); + } + } catch (RepositoryException e) { + throw new CmsException("Cannot undeepen " + getEdited(), e); + } + } + + // UI CHANGES + protected Paragraph paragraphSplitted(Paragraph paragraph, Node newNode) throws RepositoryException { + Section section = paragraph.getSection(); + updateContent(paragraph); + Paragraph newParagraph = newParagraph((TextSection) section, newNode); + newParagraph.setLayoutData(CmsUtils.fillWidth()); + newParagraph.moveBelow(paragraph); + layout(paragraph.getControl(), newParagraph.getControl()); + return newParagraph; + } + + protected Paragraph sectionTitleSplitted(SectionTitle sectionTitle, Node newNode) throws RepositoryException { + updateContent(sectionTitle); + Paragraph newParagraph = newParagraph(sectionTitle.getSection(), newNode); + // we assume beforeFirst is not null since there was a sectionTitle + newParagraph.moveBelow(sectionTitle.getSection().getHeader()); + layout(sectionTitle.getControl(), newParagraph.getControl()); + return newParagraph; + } + + protected Paragraph paragraphMergedWithPrevious(Paragraph removed, Node remaining) throws RepositoryException { + Section section = removed.getSection(); + removed.dispose(); + + Paragraph paragraph = (Paragraph) section.getSectionPart(remaining.getIdentifier()); + updateContent(paragraph); + layout(paragraph.getControl()); + return paragraph; + } + + protected void paragraphMergedWithNext(Paragraph remaining, Paragraph removed) throws RepositoryException { + removed.dispose(); + updateContent(remaining); + layout(remaining.getControl()); + } + + // UTILITIES + protected String p(Integer index) { + StringBuilder sb = new StringBuilder(6); + sb.append(DocBookNames.DBK_PARA).append('[').append(index).append(']'); + return sb.toString(); + } + + protected String h(Integer index) { + StringBuilder sb = new StringBuilder(5); + sb.append(DocBookNames.DBK_SECTION).append('[').append(index).append(']'); + return sb.toString(); + } + + // GETTERS / SETTERS + public Section getMainSection() { + return mainSection; + } + + public boolean isFlat() { + return flat; + } + + public TextInterpreter getTextInterpreter() { + return textInterpreter; + } + + // KEY LISTENER + @Override + public void keyPressed(KeyEvent ke) { + if (log.isTraceEnabled()) + log.trace(ke); + + if (getEdited() == null) + return; + boolean altPressed = (ke.stateMask & SWT.ALT) != 0; + boolean shiftPressed = (ke.stateMask & SWT.SHIFT) != 0; + boolean ctrlPressed = (ke.stateMask & SWT.CTRL) != 0; + + try { + // Common + if (ke.keyCode == SWT.ESC) { + cancelEdit(); + } else if (ke.character == '\r') { + splitEdit(); + } else if (ke.character == 'S') { + if (ctrlPressed) + saveEdit(); + } else if (ke.character == '\t') { + if (!shiftPressed) { + deepen(); + } else if (shiftPressed) { + undeepen(); + } + } else { + if (getEdited() instanceof Paragraph) { + Paragraph paragraph = (Paragraph) getEdited(); + Section section = paragraph.getSection(); + if (altPressed && ke.keyCode == SWT.ARROW_RIGHT) { + edit(section.nextSectionPart(paragraph), 0); + } else if (altPressed && ke.keyCode == SWT.ARROW_LEFT) { + edit(section.previousSectionPart(paragraph), 0); + } else if (ke.character == SWT.BS) { + Text text = (Text) paragraph.getControl(); + int caretPosition = text.getCaretPosition(); + if (caretPosition == 0) { + mergeWithPrevious(); + } + } else if (ke.character == SWT.DEL) { + Text text = (Text) paragraph.getControl(); + int caretPosition = text.getCaretPosition(); + int charcount = text.getCharCount(); + if (caretPosition == charcount) { + mergeWithNext(); + } + } + } + } + } catch (Exception e) { + ke.doit = false; + notifyEditionException(e); + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + + // MOUSE LISTENER + @Override + protected MouseListener createMouseListener() { + return new ML(); + } + + private class ML extends MouseAdapter { + private static final long serialVersionUID = 8526890859876770905L; + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (e.button == 1) { + Control source = (Control) e.getSource(); + if (getCmsEditable().canEdit()) { + if (getCmsEditable().isEditing() && !(getEdited() instanceof Img)) { + if (source == mainSection) + return; + EditablePart part = findDataParent(source); + upload(part); + } else { + getCmsEditable().startEditing(); + } + } + } + } + + @Override + public void mouseDown(MouseEvent e) { + if (getCmsEditable().isEditing()) { + if (e.button == 1) { + Control source = (Control) e.getSource(); + EditablePart composite = findDataParent(source); + Point point = new Point(e.x, e.y); + if (!(composite instanceof Img)) + edit(composite, source.toDisplay(point)); + } else if (e.button == 3) { + EditablePart composite = findDataParent((Control) e.getSource()); + if (styledTools != null) + styledTools.show(composite, new Point(e.x, e.y)); + } + } + } + + @Override + public void mouseUp(MouseEvent e) { + } + } + + // FILE UPLOAD LISTENER + private class FUL implements FileUploadListener { + public void uploadProgress(FileUploadEvent event) { + // TODO Monitor upload progress + } + + public void uploadFailed(FileUploadEvent event) { + throw new CmsException("Upload failed " + event, event.getException()); + } + + public void uploadFinished(FileUploadEvent event) { + for (FileDetails file : event.getFileDetails()) { + if (log.isDebugEnabled()) + log.debug("Received: " + file.getFileName()); + } + mainSection.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + saveEdit(); + } + }); + FileUploadHandler uploadHandler = (FileUploadHandler) event.getSource(); + uploadHandler.dispose(); + } + } +} \ No newline at end of file diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/DbkContextMenu.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/DbkContextMenu.java new file mode 100644 index 000000000..181a584c4 --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/DbkContextMenu.java @@ -0,0 +1,135 @@ +package org.argeo.cms.ui.internal.text; + +import java.util.ArrayList; +import java.util.List; + +import org.argeo.cms.CmsNames; +import org.argeo.cms.text.Paragraph; +import org.argeo.cms.text.TextStyles; +import org.argeo.cms.viewers.EditablePart; +import org.argeo.cms.viewers.SectionPart; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +/** Dialog to edit a text part. */ +class DbkContextMenu extends Shell implements CmsNames, TextStyles { + private final static String[] DEFAULT_TEXT_STYLES = { + TextStyles.TEXT_DEFAULT, TextStyles.TEXT_PRE, TextStyles.TEXT_QUOTE }; + + private final AbstractDbkViewer textViewer; + + private static final long serialVersionUID = -3826246895162050331L; + private List styleButtons = new ArrayList(); + + private Label deleteButton, publishButton, editButton; + + private EditablePart currentTextPart; + + public DbkContextMenu(AbstractDbkViewer textViewer, Display display) { + super(display, SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); + this.textViewer = textViewer; + setLayout(new GridLayout()); + setData(RWT.CUSTOM_VARIANT, TEXT_STYLED_TOOLS_DIALOG); + + StyledToolMouseListener stml = new StyledToolMouseListener(); + if (textViewer.getCmsEditable().isEditing()) { + for (String style : DEFAULT_TEXT_STYLES) { + StyleButton styleButton = new StyleButton(this, SWT.WRAP); + styleButton.setData(RWT.CUSTOM_VARIANT, style); + styleButton.setData(RWT.MARKUP_ENABLED, true); + styleButton.addMouseListener(stml); + styleButtons.add(styleButton); + } + + // Delete + deleteButton = new Label(this, SWT.NONE); + deleteButton.setText("Delete"); + deleteButton.addMouseListener(stml); + + // Publish + publishButton = new Label(this, SWT.NONE); + publishButton.setText("Publish"); + publishButton.addMouseListener(stml); + } else if (textViewer.getCmsEditable().canEdit()) { + // Edit + editButton = new Label(this, SWT.NONE); + editButton.setText("Edit"); + editButton.addMouseListener(stml); + } + addShellListener(new ToolsShellListener()); + } + + public void show(EditablePart source, Point location) { + if (isVisible()) + setVisible(false); + + this.currentTextPart = source; + + if (currentTextPart instanceof Paragraph) { + final int size = 32; + String text = textViewer + .getRawParagraphText((Paragraph) currentTextPart); + String textToShow = text.length() > size ? text.substring(0, + size - 3) + "..." : text; + for (StyleButton styleButton : styleButtons) { + styleButton.setText(textToShow); + } + } + pack(); + layout(); + if (source instanceof Control) + setLocation(((Control) source).toDisplay(location.x, location.y)); + open(); + } + + class StyleButton extends Label { + private static final long serialVersionUID = 7731102609123946115L; + + public StyleButton(Composite parent, int swtStyle) { + super(parent, swtStyle); + } + + } + + class StyledToolMouseListener extends MouseAdapter { + private static final long serialVersionUID = 8516297091549329043L; + + @Override + public void mouseDown(MouseEvent e) { + Object eventSource = e.getSource(); + if (eventSource instanceof StyleButton) { + StyleButton sb = (StyleButton) e.getSource(); + String style = sb.getData(RWT.CUSTOM_VARIANT).toString(); + textViewer + .setParagraphStyle((Paragraph) currentTextPart, style); + } else if (eventSource == deleteButton) { + textViewer.deletePart((SectionPart) currentTextPart); + } else if (eventSource == editButton) { + textViewer.getCmsEditable().startEditing(); + } else if (eventSource == publishButton) { + textViewer.getCmsEditable().stopEditing(); + } + setVisible(false); + } + } + + class ToolsShellListener extends org.eclipse.swt.events.ShellAdapter { + private static final long serialVersionUID = 8432350564023247241L; + + @Override + public void shellDeactivated(ShellEvent e) { + setVisible(false); + } + + } +} diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java index f4edb12b3..291b30c5c 100644 --- a/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java +++ b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java @@ -1,7 +1,15 @@ package org.argeo.jcr.docbook; public interface DocBookNames { - public final static String DBK_ = "dbk:"; - public final static String DBK_PARA = DBK_ + "para"; - public final static String DBK_SECTION = DBK_ + "section"; + // ELEMENTS + public final static String DBK_PARA = "dbk:para"; + public final static String DBK_SECTION = "dbk:section"; + public final static String DBK_MEDIAOBJECT = "dbk:mediaobject"; + + // ATTRIBUTES + // TODO centralise + public final static String JCR_XMLTEXT = "jcr:xmltext"; + public final static String JCR_XMLCHARACTERS = "jcr:xmlcharacters"; + + public final static String DBK_ROLE = "dbk:role"; } diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookTypes.java b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookTypes.java new file mode 100644 index 000000000..176c5c3a9 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookTypes.java @@ -0,0 +1,12 @@ +package org.argeo.jcr.docbook; + +public interface DocBookTypes { + public final static String ARTICLE = "dbk:article"; + public final static String SECTION = "dbk:section"; + public final static String PARA = "dbk:para"; + public final static String XMLTEXT = "dbk:xmltext"; + + public final static String MEDIAOBJECT = "dbk:mediaobject"; + public final static String IMAGEOBJECT = "dbk:imageobject"; + public final static String IMAGEDATA = "dbk:imagedata"; +}