diff --git a/build.gradle.kts b/build.gradle.kts index 1ae1280..7200612 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "com.huangzj" -version = "0.1.0" +version = "1.0.0" repositories { // 国内镜像,失败再走中央仓库 diff --git a/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java b/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java index b2c1fa0..d2bb598 100644 --- a/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java +++ b/src/main/java/com/huangzj/protoplugin/memo/MemoItem.java @@ -1,8 +1,13 @@ package com.huangzj.protoplugin.memo; +import java.text.SimpleDateFormat; +import java.util.Date; + /** 备忘录条目(字段需可被持久化组件序列化)。 */ public class MemoItem { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + public String id = ""; public String title = ""; public String content = ""; @@ -23,4 +28,24 @@ public class MemoItem { } return "(无标题)"; } + + public String displayDate() { + if (updated <= 0) { + return ""; + } + return DATE_FORMAT.format(new Date(updated)); + } + + public String contentPreview() { + if (content == null || content.trim().isEmpty()) { + return ""; + } + String trimmed = content.trim(); + int newlineIdx = trimmed.indexOf('\n'); + String firstLine = newlineIdx >= 0 ? trimmed.substring(0, newlineIdx) : trimmed; + if (firstLine.length() > 50) { + return firstLine.substring(0, 50) + "..."; + } + return firstLine; + } } diff --git a/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java b/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java index e342ae9..cb75068 100644 --- a/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java +++ b/src/main/java/com/huangzj/protoplugin/memo/MemoPanel.java @@ -1,6 +1,7 @@ package com.huangzj.protoplugin.memo; import com.intellij.openapi.project.Project; +import com.intellij.ui.JBColor; import com.intellij.ui.components.JBList; import com.intellij.ui.components.JBScrollPane; import com.intellij.ui.components.JBTextArea; @@ -13,13 +14,16 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Font; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; @@ -51,17 +55,25 @@ public final class MemoPanel extends JPanel { private final JPanel leftButtonBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); private final JPanel themeChooserBar = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0)); private final JPanel topStack = new JPanel(new BorderLayout()); - private final JSplitPane split; - private final JBScrollPane listScroll; - private final JBScrollPane contentScroll; + private JSplitPane split; + private JBScrollPane listScroll; + private JBScrollPane contentScroll; private final JPanel titleBlock = new JPanel(new BorderLayout(4, 4)); private final JPanel contentBlock = new JPanel(new BorderLayout(4, 4)); private final JPanel right = new JPanel(new BorderLayout(8, 8)); + private final JBTextField searchField = new JBTextField(); + private final JLabel countLabel = new JLabel(" "); + private final JLabel modifiedLabel = new JLabel(" "); + private final JPanel leftPanel = new JPanel(new BorderLayout(0, 4)); + private final JPanel searchPanel = new JPanel(new BorderLayout(4, 0)); + private final JLabel searchIcon = new JLabel("\uD83D\uDD0D"); + private final JPanel editorStatusBar = new JPanel(new BorderLayout()); private boolean loadingSelection; private boolean themeComboSuppress; private MemoUiTheme currentTheme = MemoUiTheme.IDE_DEFAULT; private final Font defaultLabelFont; + private List allItems = new ArrayList<>(); public MemoPanel(@NotNull Project project) { super(new BorderLayout(8, 8)); @@ -69,7 +81,25 @@ public final class MemoPanel extends JPanel { defaultLabelFont = titleLabel.getFont(); setBorder(JBUI.Borders.empty(8)); + initToolbar(); + initFlair(); + initTopStack(); + initSearchPanel(); + initList(); + initLeftPanel(); + initEditor(); + initSplit(); + initTheme(); + initListeners(); + + reloadListModel(); + } + + private void initToolbar() { leftButtonBar.setOpaque(false); + newBtn.setToolTipText("新建一条备忘录"); + saveBtn.setToolTipText("保存当前备忘录"); + deleteBtn.setToolTipText("删除选中的备忘录"); leftButtonBar.add(newBtn); leftButtonBar.add(saveBtn); leftButtonBar.add(deleteBtn); @@ -81,21 +111,48 @@ public final class MemoPanel extends JPanel { toolbar.setOpaque(false); toolbar.add(leftButtonBar, BorderLayout.WEST); toolbar.add(themeChooserBar, BorderLayout.EAST); + } + private void initFlair() { flairLabel.setOpaque(true); flairLabel.setFont(flairLabel.getFont().deriveFont(Font.PLAIN, 12f)); + } + private void initTopStack() { topStack.setOpaque(false); topStack.add(flairLabel, BorderLayout.NORTH); topStack.add(toolbar, BorderLayout.CENTER); add(topStack, BorderLayout.NORTH); + } + private void initSearchPanel() { + searchField.getEmptyText().setText("搜索备忘录..."); + searchField.setToolTipText("输入关键字过滤备忘录标题和内容"); + searchPanel.setOpaque(false); + searchPanel.add(searchIcon, BorderLayout.WEST); + searchPanel.add(searchField, BorderLayout.CENTER); + searchPanel.setBorder(JBUI.Borders.empty(0, 0, 4, 0)); + } + + private void initList() { list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); installListRenderer(); - listScroll = new JBScrollPane(list); listScroll.setPreferredSize(new Dimension(200, 300)); + countLabel.setFont(countLabel.getFont().deriveFont(Font.PLAIN, 11f)); + countLabel.setHorizontalAlignment(SwingConstants.RIGHT); + countLabel.setBorder(JBUI.Borders.empty(2, 4, 0, 4)); + } + + private void initLeftPanel() { + leftPanel.setOpaque(false); + leftPanel.add(searchPanel, BorderLayout.NORTH); + leftPanel.add(listScroll, BorderLayout.CENTER); + leftPanel.add(countLabel, BorderLayout.SOUTH); + } + + private void initEditor() { contentArea.setLineWrap(true); contentArea.setWrapStyleWord(true); contentScroll = new JBScrollPane(contentArea); @@ -108,22 +165,34 @@ public final class MemoPanel extends JPanel { contentBlock.add(contentLabel, BorderLayout.NORTH); contentBlock.add(contentScroll, BorderLayout.CENTER); + modifiedLabel.setFont(modifiedLabel.getFont().deriveFont(Font.PLAIN, 11f)); + modifiedLabel.setBorder(JBUI.Borders.empty(2, 0, 0, 0)); + editorStatusBar.setOpaque(false); + editorStatusBar.add(modifiedLabel, BorderLayout.EAST); + right.setOpaque(false); right.add(titleBlock, BorderLayout.NORTH); right.add(contentBlock, BorderLayout.CENTER); + right.add(editorStatusBar, BorderLayout.SOUTH); titleField.setPreferredSize(new Dimension(200, 28)); + } - split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, listScroll, right); + private void initSplit() { + split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, right); split.setResizeWeight(0.28); split.setOpaque(false); add(split, BorderLayout.CENTER); + } + private void initTheme() { MemoUiTheme initial = MemoProjectService.getInstance(project).getMemoTheme(); themeComboSuppress = true; themeCombo.setSelectedItem(initial); themeComboSuppress = false; applyTheme(initial); + } + private void initListeners() { themeCombo.addActionListener( e -> { if (themeComboSuppress) { @@ -136,8 +205,6 @@ public final class MemoPanel extends JPanel { } }); - reloadListModel(); - list.addListSelectionListener( new ListSelectionListener() { @Override @@ -172,6 +239,26 @@ public final class MemoPanel extends JPanel { refreshSelectedTitleInList(); } }); + + searchField + .getDocument() + .addDocumentListener( + new javax.swing.event.DocumentListener() { + @Override + public void insertUpdate(javax.swing.event.DocumentEvent e) { + filterList(); + } + + @Override + public void removeUpdate(javax.swing.event.DocumentEvent e) { + filterList(); + } + + @Override + public void changedUpdate(javax.swing.event.DocumentEvent e) { + filterList(); + } + }); } private void installListRenderer() { @@ -184,27 +271,57 @@ public final class MemoPanel extends JPanel { int index, boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent( + JLabel label = (JLabel) super.getListCellRendererComponent( jList, value, index, isSelected, cellHasFocus); if (value instanceof MemoItem) { - setText(((MemoItem) value).displayTitle()); + MemoItem item = (MemoItem) value; + String title = escapeHtml(item.displayTitle()); + String date = item.displayDate(); + String preview = item.contentPreview(); + + StringBuilder html = new StringBuilder(128); + html.append(""); + html.append("").append(title).append(""); + if (!date.isEmpty()) { + html.append("
").append(date).append(""); + } + if (!preview.isEmpty()) { + html.append("
").append(escapeHtml(preview)).append(""); + } + html.append(""); + label.setText(html.toString()); } if (currentTheme != MemoUiTheme.IDE_DEFAULT) { MemoThemeSpec spec = MemoThemeSpec.forTheme(currentTheme); - setOpaque(true); + label.setOpaque(true); if (isSelected) { - setBackground(spec.listSelectedBg); - setForeground(spec.listSelectedFg); + label.setBackground(spec.listSelectedBg); + label.setForeground(spec.listSelectedFg); } else { - setBackground(spec.listBg); - setForeground(spec.text); + label.setBackground(spec.listBg); + label.setForeground(spec.text); } } - return this; + label.setBorder(JBUI.Borders.empty(4, 8, 4, 8)); + return label; } }); } + private static String escapeHtml(String text) { + if (text == null) { + return ""; + } + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """); + } + private void applyTheme(@NotNull MemoUiTheme theme) { currentTheme = theme; installListRenderer(); @@ -235,6 +352,9 @@ public final class MemoPanel extends JPanel { flairLabel.setBackground(panelBg); flairLabel.setForeground(spec.text); split.setBackground(panelBg); + leftPanel.setBackground(panelBg); + searchPanel.setBackground(panelBg); + searchIcon.setForeground(spec.text); listScroll.setBackground(panelBg); listScroll.getViewport().setBackground(spec.listBg); list.setBackground(spec.listBg); @@ -244,12 +364,15 @@ public final class MemoPanel extends JPanel { contentBlock.setBackground(panelBg); titleLabel.setForeground(spec.text); contentLabel.setForeground(spec.text); + countLabel.setForeground(spec.text); + modifiedLabel.setForeground(spec.text); + editorStatusBar.setBackground(panelBg); if (theme == MemoUiTheme.CUTE_CANDY) { flairLabel.setVisible(true); - flairLabel.setText("✨ 今天也要甜甜哒~ 选一条备忘,把想法留下来吧 💕"); - titleLabel.setText("📝 标题"); - contentLabel.setText("💗 正文"); + flairLabel.setText("\u2728 今天也要甜甜哒\uFF5E 选一条备忘,把想法留下来吧 \uD83D\uDC95"); + titleLabel.setText("\uD83D\uDCDD 标题"); + contentLabel.setText("\uD83D\uDC97 正文"); titleLabel.setFont(defaultLabelFont.deriveFont(Font.BOLD, defaultLabelFont.getSize() + 1f)); contentLabel.setFont( defaultLabelFont.deriveFont(Font.BOLD, defaultLabelFont.getSize() + 1f)); @@ -263,6 +386,7 @@ public final class MemoPanel extends JPanel { } styleTextField(titleField, spec, softField); + styleTextField(searchField, spec, softField); styleTextArea(contentArea, contentScroll, spec, softField); styleAccentButton(newBtn, spec); @@ -281,6 +405,7 @@ public final class MemoPanel extends JPanel { Color editorBg = UIUtil.getTextFieldBackground(); Color fg = UIUtil.getTextFieldForeground(); Color listBg = UIUtil.getListBackground(); + Color labelFg = UIUtil.getLabelForeground(); setBackground(panelBg); topStack.setOpaque(false); @@ -289,12 +414,15 @@ public final class MemoPanel extends JPanel { toolbar.setOpaque(false); leftButtonBar.setOpaque(false); themeChooserBar.setOpaque(false); - themeChooserLabel.setForeground(UIUtil.getLabelForeground()); + themeChooserLabel.setForeground(labelFg); flairLabel.setVisible(false); flairLabel.setText(" "); flairLabel.setBackground(panelBg); - flairLabel.setForeground(UIUtil.getLabelForeground()); + flairLabel.setForeground(labelFg); split.setBackground(panelBg); + leftPanel.setBackground(panelBg); + searchPanel.setBackground(panelBg); + searchIcon.setForeground(labelFg); listScroll.setBackground(panelBg); listScroll.getViewport().setBackground(listBg); list.setBackground(listBg); @@ -306,14 +434,19 @@ public final class MemoPanel extends JPanel { contentLabel.setText("内容"); titleLabel.setFont(defaultLabelFont); contentLabel.setFont(defaultLabelFont); - titleLabel.setForeground(UIUtil.getLabelForeground()); - contentLabel.setForeground(UIUtil.getLabelForeground()); + titleLabel.setForeground(labelFg); + contentLabel.setForeground(labelFg); + countLabel.setForeground(JBColor.GRAY); + modifiedLabel.setForeground(JBColor.GRAY); + editorStatusBar.setBackground(panelBg); resetFieldLook(titleField, editorBg, fg); + resetFieldLook(searchField, editorBg, fg); resetFieldLook(contentArea, editorBg, fg); contentScroll.getViewport().setBackground(editorBg); contentArea.setCaretColor(fg); titleField.setCaretColor(fg); + searchField.setCaretColor(fg); resetButton(newBtn); resetButton(saveBtn); @@ -392,19 +525,68 @@ public final class MemoPanel extends JPanel { } private void reloadListModel() { + allItems = MemoProjectService.getInstance(project).getItems(); + filterList(); + } + + private void filterList() { + String query = searchField.getText().trim().toLowerCase(); listModel.clear(); - List items = MemoProjectService.getInstance(project).getItems(); - for (MemoItem m : items) { + + List filtered; + if (query.isEmpty()) { + filtered = allItems; + } else { + filtered = allItems.stream() + .filter(m -> matchesQuery(m, query)) + .collect(Collectors.toList()); + } + + for (MemoItem m : filtered) { listModel.addElement(m); } - if (!items.isEmpty() && list.getSelectedIndex() < 0) { + + updateCountLabel(filtered.size()); + + if (!filtered.isEmpty() && list.getSelectedIndex() < 0) { list.setSelectedIndex(0); } + + if (filtered.isEmpty() && !allItems.isEmpty() && !query.isEmpty()) { + clearEditor(); + modifiedLabel.setText("无匹配结果"); + } else if (filtered.isEmpty() && allItems.isEmpty()) { + clearEditor(); + modifiedLabel.setText(" "); + } + } + + private static boolean matchesQuery(MemoItem m, String query) { + if (m.title != null && m.title.toLowerCase().contains(query)) { + return true; + } + if (m.content != null && m.content.toLowerCase().contains(query)) { + return true; + } + return false; + } + + private void updateCountLabel(int visibleCount) { + int total = allItems.size(); + if (total == 0) { + countLabel.setText(" "); + } else if (visibleCount == total) { + countLabel.setText("共 " + total + " 条"); + } else { + countLabel.setText("显示 " + visibleCount + " / " + total + " 条"); + } } private void createMemo() { MemoItem m = MemoProjectService.getInstance(project).createMemo(); - listModel.addElement(m); + allItems = MemoProjectService.getInstance(project).getItems(); + searchField.setText(""); + filterList(); list.setSelectedValue(m, true); titleField.requestFocusInWindow(); } @@ -417,7 +599,9 @@ public final class MemoPanel extends JPanel { m.title = titleField.getText(); m.content = contentArea.getText(); m.updated = System.currentTimeMillis(); + allItems = MemoProjectService.getInstance(project).getItems(); list.repaint(); + updateModifiedLabel(m); } private void deleteCurrent() { @@ -425,9 +609,21 @@ public final class MemoPanel extends JPanel { if (m == null) { return; } + String itemTitle = m.displayTitle(); + int result = JOptionPane.showConfirmDialog( + this, + "确定要删除「" + itemTitle + "」吗?\n此操作不可撤销。", + "确认删除", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); + if (result != JOptionPane.YES_OPTION) { + return; + } int idx = list.getSelectedIndex(); MemoProjectService.getInstance(project).removeById(m.id); + allItems = MemoProjectService.getInstance(project).getItems(); listModel.removeElement(m); + updateCountLabel(listModel.getSize()); if (!listModel.isEmpty()) { int next = Math.min(idx, listModel.getSize() - 1); list.setSelectedIndex(Math.max(0, next)); @@ -445,15 +641,26 @@ public final class MemoPanel extends JPanel { } else { titleField.setText(m.title != null ? m.title : ""); contentArea.setText(m.content != null ? m.content : ""); + updateModifiedLabel(m); } } finally { loadingSelection = false; } } + private void updateModifiedLabel(MemoItem m) { + String date = m.displayDate(); + if (!date.isEmpty()) { + modifiedLabel.setText("最后修改: " + date); + } else { + modifiedLabel.setText(" "); + } + } + private void clearEditor() { titleField.setText(""); contentArea.setText(""); + modifiedLabel.setText(" "); } private void refreshSelectedTitleInList() { @@ -462,4 +669,4 @@ public final class MemoPanel extends JPanel { } javax.swing.SwingUtilities.invokeLater(() -> list.repaint()); } -} +} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a8b835a..964afd1 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,6 +1,6 @@ com.huangzj.protoPlugin - Idea-Plugin + Huangzhijun-Idea-Plugin huangzj