001: /*
002: * QueryEditorTextPane.java
003: *
004: * Copyright (C) 2002, 2003, 2004, 2005, 2006 Takis Diakoumis
005: *
006: * This program is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU General Public License
008: * as published by the Free Software Foundation; either version 2
009: * of the License, or any later version.
010: *
011: * This program is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: * GNU General Public License for more details.
015: *
016: * You should have received a copy of the GNU General Public License
017: * along with this program; if not, write to the Free Software
018: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
019: *
020: */
021:
022: package org.executequery.gui.editor;
023:
024: import java.awt.Cursor;
025: import java.awt.Graphics;
026: import java.awt.Insets;
027: import java.awt.Rectangle;
028: import java.awt.event.FocusEvent;
029: import java.awt.event.FocusListener;
030: import java.awt.event.KeyEvent;
031:
032: import javax.swing.JComponent;
033: import javax.swing.JTextPane;
034: import javax.swing.UIManager;
035: import javax.swing.event.CaretEvent;
036: import javax.swing.event.CaretListener;
037: import javax.swing.event.DocumentEvent;
038: import javax.swing.event.DocumentListener;
039: import javax.swing.text.BadLocationException;
040: import javax.swing.text.DefaultCaret;
041: import javax.swing.text.DefaultStyledDocument;
042: import javax.swing.text.Document;
043: import javax.swing.text.Element;
044: import javax.swing.text.JTextComponent;
045:
046: import org.executequery.Constants;
047: import org.executequery.GUIUtilities;
048: import org.executequery.KeywordProperties;
049: import org.underworldlabs.util.SystemProperties;
050: import org.executequery.gui.UndoableComponent;
051: import org.executequery.databasemediators.DatabaseConnection;
052: import org.executequery.components.LineNumber;
053: import org.executequery.gui.text.SQLTextPane;
054: import org.executequery.gui.text.TextUndoManager;
055: import org.underworldlabs.util.MiscUtils;
056:
057: /* ----------------------------------------------------------
058: * CVS NOTE: Changes to the CVS repository prior to the
059: * release of version 3.0.0beta1 has meant a
060: * resetting of CVS revision numbers.
061: * ----------------------------------------------------------
062: */
063:
064: /**
065: * The SQL text area for the Query Editor.
066: *
067: * @author Takis Diakoumis
068: * @version $Revision: 1.5 $
069: * @date $Date: 2006/05/14 06:56:52 $
070: */
071: public class QueryEditorTextPane extends SQLTextPane implements
072: UndoableComponent, CaretListener, FocusListener,
073: DocumentListener {
074:
075: /** The editor panel containing this text component */
076: private QueryEditorTextPanel editorPanel;
077:
078: /** To display line numbers */
079: private LineNumber lineBorder;
080:
081: /** The text pane's undo manager */
082: protected TextUndoManager undoManager;
083:
084: /**
085: * Constructs a new text pane with the specified
086: * <code>QueryEditorTextPanel</code> as its parent container.
087: *
088: * @param the parent container
089: */
090: public QueryEditorTextPane(QueryEditorTextPanel editorPanel) {
091: this .editorPanel = editorPanel;
092: try {
093: jbInit();
094: } catch (Exception e) {
095: e.printStackTrace();
096: }
097: }
098:
099: private void jbInit() throws Exception {
100: setMargin(new Insets(2, 2, 2, 2));
101:
102: if (editorPanel == null) {
103: setEditorPreferences();
104: }
105:
106: // add the line number border and caret listener
107: lineBorder = new LineNumber(this );
108: addCaretListener(this );
109:
110: // undo functionality
111: undoManager = new TextUndoManager(this );
112: undoManager.setLimit(SystemProperties.getIntProperty("user",
113: "editor.undo.count"));
114:
115: document.addDocumentListener(this );
116: addFocusListener(this );
117:
118: setDragEnabled(true);
119: setRequestFocusEnabled(true);
120: setFocusable(true);
121:
122: // set the caret
123: EditorCaret caret = new EditorCaret();
124: int blinkRate = UIManager.getInt("TextPane.caretBlinkRate");
125: if (blinkRate > 0) {
126: caret.setBlinkRate(blinkRate);
127: } else {
128: caret.setBlinkRate(500);
129: }
130: setCaret(caret);
131:
132: // set to insert mode
133: document.setInsertMode(QueryEditorConstants.INSERT_MODE);
134: }
135:
136: public void showLineNumbers(boolean show) {
137: lineBorder.getParent().setVisible(show);
138: }
139:
140: public void disableUpdates(boolean disable) {
141: if (disable) {
142: addUndoEdit();
143: String text = getText();
144: setDocument(new DefaultStyledDocument());
145: setText(text);
146: disableCaretUpdate(true);
147: } else {
148: String text = getText();
149: setDocument(document);
150: setText(text);
151: disableCaretUpdate(false);
152: }
153: }
154:
155: public void disableCaretUpdate(boolean disable) {
156:
157: if (disable) {
158: removeCaretListener(this );
159: } else {
160: boolean hasListener = false;
161: CaretListener[] caretListners = getCaretListeners();
162:
163: for (int i = 0; i < caretListners.length; i++) {
164:
165: if (caretListners[i] == this ) {
166: hasListener = true;
167: break;
168: }
169:
170: }
171:
172: if (!hasListener) {
173: addCaretListener(this );
174: caretUpdate(null);
175: }
176:
177: }
178:
179: }
180:
181: /**
182: * Clears the undo managers stored edits
183: */
184: protected void clearEdits() {
185: undoManager.discardAllEdits();
186: }
187:
188: /**
189: * Removes (designed to be temporary) listeners attached
190: * to this text pane including the caret updates and
191: * undo/redo listener objects.
192: */
193: protected void uninstallListeners() {
194: removeCaretListener(this );
195: document.removeDocumentListener(this );
196: undoManager.suspend();
197: }
198:
199: /**
200: * Reinstates listeners attached to this text pane
201: * including the caret updates and undo/redo listener objects.
202: */
203: protected void reinstallListeners() {
204: addCaretListener(this );
205: document.addDocumentListener(this );
206: undoManager.reinstate();
207: }
208:
209: /**
210: * Loads the specified text into a blank 'offscreen' document
211: * before switching to the SQL document. This is most effective
212: * for very large chunks of text loaded from file or similar.
213: */
214: public void loadText(String text) {
215: setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
216: try {
217: // uninstall listeners on the text pane
218: uninstallListeners();
219: // clear the current held edits
220: clearEdits();
221:
222: // create a dummy document to load the text into
223: Document _document = new DefaultStyledDocument();
224: setDocument(_document);
225:
226: try {
227: // clear the contents of we have any
228: int length = document.getLength();
229: if (length > 0) {
230: // replace the existing text
231: document.replace(0, length, text, null);
232: } else {
233: // set the new text
234: document.insertString(0, text, null);
235: }
236:
237: } catch (BadLocationException e) {
238: }
239:
240: // reset the SQL document
241: setDocument(document);
242: } finally {
243: // update the line border state
244: updateLineBorder();
245: // reinstall listeners on the text pane
246: reinstallListeners();
247: // reset the caret
248: setCaretPosition(0);
249: // reset the cursor
250: setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
251: }
252: }
253:
254: /**
255: * Override to update the line border.
256: */
257: public void setText(String text) {
258: super .setText(text);
259: updateLineBorder();
260: }
261:
262: /**
263: * Override to return false.
264: */
265: public boolean isOpaque() {
266: return false;
267: }
268:
269: /**
270: * Paints the current line highlight and right-hand margin
271: * before a call to the super class.
272: *
273: * @param g the <code>Graphics</code> object to protect
274: */
275: public void paintComponent(Graphics g) {
276: int height = getHeight();
277: int width = getWidth();
278:
279: g.setColor(getBackground());
280: g.fillRect(0, 0, width, height);
281:
282: // paint the current line highlight
283: if (QueryEditorSettings.isDisplayLineHighlight()) {
284: int currentRow = getCurrentCursorRow();
285: g.setColor(QueryEditorSettings.getLineHighlightColour());
286: g.fillRect(1, (currentRow * fontHeight) + 2, width - 1,
287: fontHeight);
288: }
289:
290: // paint the right-hand margin
291: if (QueryEditorSettings.isDisplayRightMargin()) {
292: int xPosn = fontWidth
293: * QueryEditorSettings.getRightMarginSize();
294: g.setColor(QueryEditorSettings.getRightMarginColour());
295: g.drawLine(xPosn, 0, xPosn, height);
296: }
297:
298: super .paintComponent(g);
299: }
300:
301: public void setQueryAreaText(String s) {
302: setText(s);
303: }
304:
305: /**
306: * Shifts the text at position start to position end left.
307: *
308: * @param start - the start offset
309: * @param end - the end offset
310: */
311: public void shiftTextLeft(int start, int end) {
312: addUndoEdit();
313: getSQLSyntaxDocument().shiftTabEvent(start, end, false);
314: }
315:
316: /**
317: * Shifts the text at position start to the right.
318: * This will usually be the start position of any
319: * particular row.
320: *
321: * @param start - the start offset
322: */
323: public void shiftTextRight(int offset) {
324: addUndoEdit();
325: insertTextAtOffset(offset, "\t");
326: }
327:
328: /**
329: * Inserts the specified text at the offset.
330: *
331: * @param offset - the insertion point
332: * @param text - the text
333: */
334: public void insertTextAtOffset(int offset, String text) {
335: try {
336: getDocument().insertString(offset, text, null);
337: } catch (BadLocationException e) {
338: }
339: }
340:
341: public JTextPane getQueryArea() {
342: return this ;
343: }
344:
345: public JComponent getLineBorder() {
346: return lineBorder;
347: }
348:
349: public void goToRow(int row) {
350: int goToRow = getRowPosition(row - 1);
351: if (goToRow < 0) {
352: GUIUtilities
353: .displayErrorMessage("The line number entered is invalid.");
354: return;
355: }
356: setCaretPosition(goToRow);
357: }
358:
359: protected void setEditorPreferences() {
360: // call to super class
361: super .setEditorPreferences();
362: // set the pane background
363: setBackground(QueryEditorSettings.getEditorBackground());
364: }
365:
366: public void setSQLKeywords(boolean reset) {
367: document.setSQLKeywords(KeywordProperties.getSQLKeywords(),
368: reset);
369: }
370:
371: public void resetAttributeSets() {
372: String text = getText();
373: setEditorPreferences();
374: document.resetAttributeSets();
375: lineBorder.updatePreferences(QueryEditorSettings
376: .getEditorFont());
377: lineBorder.repaint();
378: setText(text);
379: }
380:
381: /**
382: * Returns the query around the specified (cursor) position.
383: *
384: * @param the position
385: * @return the query around the specified position
386: */
387: private String getQueryAt(int position) {
388: String text = getText();
389: //Log.debug("position: " + position);
390: if (MiscUtils.isNull(text)) {
391: return Constants.EMPTY;
392: }
393:
394: char[] chars = text.toCharArray();
395:
396: if (position == chars.length) {
397: position--;
398: }
399:
400: int start = -1;
401: int end = -1;
402: boolean wasSpaceChar = false;
403:
404: // determine the start point
405: for (int i = position; i >= 0; i--) {
406: if (chars[i] == Constants.NEW_LINE_CHAR) {
407:
408: if (i == 0 || wasSpaceChar) {
409: break;
410: } else if (start != -1) {
411: if (chars[i - 1] == Constants.NEW_LINE_CHAR) {
412: break;
413: } else if (Character.isSpaceChar(chars[i - 1])) {
414: wasSpaceChar = true;
415: i--;
416: }
417: }
418:
419: } else if (!Character.isSpaceChar(chars[i])) {
420: wasSpaceChar = false;
421: start = i;
422: }
423:
424: }
425:
426: if (start < 0) { // text not found
427: for (int j = 0; j < chars.length; j++) {
428: if (!Character.isWhitespace(chars[j])) {
429: start = j;
430: break;
431: }
432: }
433: }
434:
435: // determine the end point
436: for (int i = start; i < chars.length; i++) {
437:
438: if (chars[i] == Constants.NEW_LINE_CHAR) {
439:
440: if (i == chars.length - 1 || wasSpaceChar) {
441: if (end == -1) {
442: end = i;
443: }
444: break;
445: } else if (end != -1) {
446: if (chars[i + 1] == Constants.NEW_LINE_CHAR) {
447: break;
448: } else if (Character.isSpaceChar(chars[i + 1])) {
449: wasSpaceChar = true;
450: i++;
451: }
452: }
453:
454: } else if (!Character.isSpaceChar(chars[i])) {
455: end = i;
456: wasSpaceChar = false;
457: }
458: }
459:
460: //Log.debug("start: " + start + " end: " + end);
461:
462: String query = text.substring(start, end + 1);
463: //Log.debug(query);
464:
465: if ((MiscUtils.isNull(query) && start != 0) || start == end) {
466: return getQueryAt(start);
467: }
468:
469: return query;
470: }
471:
472: // ----------------------------------------
473: // DocumentListener implementation
474: // ----------------------------------------
475:
476: /**
477: * Does nothing.
478: */
479: public void changedUpdate(DocumentEvent e) {
480: }
481:
482: /**
483: * Notifies the parent QueryPanel that the text content
484: * has changed and resets the line number border panel.
485: *
486: * @param the event object
487: */
488: public void insertUpdate(DocumentEvent e) {
489: editorPanel.setContentChanged(true);
490: lineBorder.resetExecutingLine();
491: }
492:
493: /**
494: * Notifies the parent QueryPanel that the text content
495: * has changed and resets the line number border panel.
496: *
497: * @param the event object
498: */
499: public void removeUpdate(DocumentEvent e) {
500: insertUpdate(e);
501: }
502:
503: // ----------------------------------------
504:
505: /**
506: * Resets the executing line within the line
507: * number border panel.
508: */
509: public void resetExecutingLine() {
510: lineBorder.resetExecutingLine();
511: }
512:
513: /**
514: * Executes the query determined to be around the current
515: * cursor position using the database connection object
516: * specified.
517: *
518: * @param the database connection object to execute the query
519: */
520: public void executeSQLAtCursor(DatabaseConnection dc) {
521: String query = getQueryAt(getCaretPosition());
522: if (MiscUtils.isNull(query)) {
523: return;
524: }
525:
526: int index = getText().indexOf(query);
527: if (query.charAt(0) == Constants.NEW_LINE_CHAR) {
528: index++;
529: }
530:
531: lineBorder.setExecutingLine(getRowAt(index));
532: lineBorder.repaint();
533: editorPanel.executeSQLQuery(dc, query);
534: }
535:
536: /**
537: * Returns the text as contained within the editor's
538: * text panel.
539: *
540: * @return the editor's text
541: */
542: public String getQueryAreaText() {
543: return getText();
544: }
545:
546: /**
547: * Overrides <code>processKeyEvent</code> to additional process events.
548: */
549: protected void processKeyEvent(KeyEvent e) {
550:
551: if (e.getID() == KeyEvent.KEY_PRESSED) {
552:
553: int keyCode = e.getKeyCode();
554:
555: // add the processing for SHIFT-TAB
556: if (e.isShiftDown() && keyCode == KeyEvent.VK_TAB) {
557: addUndoEdit();
558: int currentPosition = getCurrentPosition();
559: int selectionStart = getSelectionStart();
560: int selectionEnd = getSelectionEnd();
561:
562: if (selectionStart == selectionEnd) {
563:
564: int newPosition = currentPosition
565: - QueryEditorSettings.getTabSize();
566: int currentRowPosition = getCurrentRowStart();
567:
568: if (!isAtStartOfRow()) {
569:
570: if (newPosition < 0) {
571: setCaretPosition(0);
572: } else if (newPosition < currentRowPosition) {
573: setCaretPosition(currentRowPosition);
574: } else {
575: setCaretPosition(newPosition);
576: }
577:
578: }
579:
580: } else {
581: document
582: .shiftTabEvent(selectionStart, selectionEnd);
583: }
584:
585: }
586:
587: // toggle insert mode on the document
588: else if (keyCode == KeyEvent.VK_INSERT) {
589: int insertMode = document.getInsertMode();
590: if (insertMode == QueryEditorConstants.INSERT_MODE) {
591: document
592: .setInsertMode(QueryEditorConstants.OVERWRITE_MODE);
593: editorPanel.getStatusBar().setInsertionMode("OVR");
594: } else {
595: document
596: .setInsertMode(QueryEditorConstants.INSERT_MODE);
597: editorPanel.getStatusBar().setInsertionMode("INS");
598: }
599: ((EditorCaret) getCaret()).modeChanged();
600: }
601:
602: }
603:
604: super .processKeyEvent(e);
605: updateLineBorder();
606: }
607:
608: /** the last element count for line border updates */
609: private int lastElementCount;
610:
611: /**
612: * Updates the line border values.
613: */
614: private void updateLineBorder() {
615: int elementCount = document.getDefaultRootElement()
616: .getElementCount();
617: if (elementCount != lastElementCount) {
618: lineBorder.setRowCount(elementCount);
619: lastElementCount = elementCount;
620: }
621: }
622:
623: /**
624: * Cuts the editor's selected text.
625: */
626: public void cut() {
627: addUndoEdit();
628: super .cut();
629: }
630:
631: /**
632: * Pastes any previously copied/cut text into the
633: * editor at the cursor or mouse pointer position.
634: */
635: public void paste() {
636: addUndoEdit();
637: super .paste();
638: }
639:
640: /**
641: * Returns true if an undo operation would be
642: * successful now, false otherwise.
643: */
644: protected boolean canUndo() {
645: return undoManager.canUndo();
646: }
647:
648: /**
649: * Completes a compound edit and adds an
650: * undoable edit to the undo manager.
651: */
652: protected void addUndoEdit() {
653: undoManager.addUndoEdit();
654: }
655:
656: /**
657: * Executes the undo action.
658: */
659: public void undo() {
660: undoManager.undo();
661: updateLineBorder();
662: }
663:
664: /**
665: * Executes the redo action.
666: */
667: public void redo() {
668: undoManager.redo();
669: updateLineBorder();
670: }
671:
672: // ----------------------------------------
673: // FocusListener implementation
674: // ----------------------------------------
675:
676: /**
677: * Updates the state of undo/redo ona focus gain.
678: */
679: public void focusGained(FocusEvent e) {
680: if (e.getSource() != this && editorPanel != null) {
681: editorPanel.focusGained();
682: }
683: }
684:
685: /**
686: * Updates the state of undo/redo on a focus lost.
687: */
688: public void focusLost(FocusEvent e) {
689: if (editorPanel != null) {
690: editorPanel.focusLost();
691: }
692: }
693:
694: // ----------------------------------------
695:
696: private int currentRow = 0;
697: private int currentPosition = 0;
698:
699: /**
700: * Returns the row number at the specified position.
701: *
702: * @param position - the position
703: */
704: protected int getRowAt(int position) {
705: Element map = getElementMap();
706: return map.getElementIndex(position);
707: }
708:
709: /**
710: * Called when the caret position is updated.
711: *
712: * @param e the caret event
713: */
714: public void caretUpdate(CaretEvent ce) {
715: super .caretUpdate(ce);
716: currentPosition = getCaretPosition();
717:
718: Element map = getElementMap();
719: int row = map.getElementIndex(currentPosition);
720:
721: if (currentRow != row) {
722: currentRow = row;
723: //lineBorder.setRowCount(map.getElementCount());
724: }
725:
726: Element lineElem = map.getElement(row);
727: int col = currentPosition - lineElem.getStartOffset();
728: editorPanel.getStatusBar().setCaretPosition(row + 1, col + 1);
729:
730: repaint();
731: }
732:
733: protected boolean isAtStartOfRow() {
734: return currentPosition == getRowPosition(currentRow);
735: }
736:
737: /**
738: * Returns the start offset of the current row.
739: *
740: * @return the current row start offset
741: */
742: protected int getCurrentRowStart() {
743: return getElementMap().getElement(currentRow).getStartOffset();
744: }
745:
746: /**
747: * Returns the end offset of the current row.
748: *
749: * @return the current row end offset
750: */
751: protected int getCurrentRowEnd() {
752: return getElementMap().getElement(currentRow).getEndOffset();
753: }
754:
755: /**
756: * Returns the start offset of the specified row.
757: *
758: * @param row - the row
759: * @return the start offset of row
760: */
761: protected int getRowStartOffset(int row) {
762: try {
763: return getElementMap().getElement(row).getStartOffset();
764: } catch (Exception e) { // where row passed is dumb value
765: return -1;
766: }
767: }
768:
769: /**
770: * Returns the end offset of the specified row.
771: *
772: * @param row - the row
773: * @return the end offset of row
774: */
775: protected int getRowEndOffset(int row) {
776: try {
777: return getElementMap().getElement(row).getEndOffset();
778: } catch (Exception e) { // where row passed is dumb value
779: return -1;
780: }
781: }
782:
783: /**
784: * Returns the start offset of the specified row.
785: *
786: * @param row - a row in the editor
787: * @return the start offset of row
788: */
789: protected int getRowPosition(int row) {
790: try {
791: return getElementMap().getElement(row).getStartOffset();
792: } catch (NullPointerException nullExc) {
793: return -1;
794: }
795: }
796:
797: /**
798: * Returns the document's root element
799: */
800: protected Element getElementMap() {
801: return getDocument().getDefaultRootElement();
802: }
803:
804: /**
805: * Returns the current caret position.
806: */
807: protected int getCurrentPosition() {
808: return currentPosition;
809: }
810:
811: /**
812: * Returns the row number of the current cursor position.
813: *
814: * @return the current caret row number
815: */
816: protected int getCurrentCursorRow() {
817: return currentRow;
818: }
819:
820: class EditorCaret extends DefaultCaret {
821:
822: void modeChanged() {
823: repaint();
824: }
825:
826: public void paint(Graphics g) {
827: if (document.getInsertMode() == QueryEditorConstants.INSERT_MODE) {
828: super .paint(g);
829: return;
830: }
831: JTextComponent comp = getComponent();
832:
833: char c;
834: int dot = getDot();
835: Rectangle r = null;
836: try {
837: r = comp.modelToView(dot);
838: if (r == null) {
839: return;
840: }
841: c = comp.getText(dot, 1).charAt(0);
842: } catch (BadLocationException e) {
843: return;
844: }
845:
846: // erase provious caret
847: if ((x != r.x) || (y != r.y)) {
848: repaint();
849: x = r.x;
850: y = r.y;
851: height = r.height;
852: }
853:
854: g.setColor(comp.getCaretColor());
855: g.setXORMode(comp.getBackground());
856:
857: width = g.getFontMetrics().charWidth(c);
858: if (c == '\t' || c == '\n') {
859: width = g.getFontMetrics().charWidth('W');
860: }
861:
862: if (isVisible()) {
863: g.fillRect(r.x, r.y, width, r.height);
864: }
865:
866: }
867:
868: }
869:
870: }
|