From 43e333d3fa4ec0b0e9e34a22fe274ba81a45d1e1 Mon Sep 17 00:00:00 2001 From: yeo-li Date: Mon, 24 Nov 2025 17:43:38 +0900 Subject: [PATCH] =?UTF-8?q?refactor(DevLogPanel):=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/github/yeoli/devlog/ui/DevLogPanel.kt | 789 +++--------------- .../github/yeoli/devlog/ui/MemoDetailPanel.kt | 188 +++++ .../yeoli/devlog/ui/MemoInputComposer.kt | 124 +++ .../github/yeoli/devlog/ui/MemoListView.kt | 2 +- .../com/github/yeoli/devlog/ui/NotePanel.kt | 154 ++++ .../yeoli/devlog/ui/SelectionStatusPanel.kt | 43 + .../devlog/ui/action/DeleteSelectedAction.kt | 57 ++ .../ui/{ => action}/MemoExportAction.kt | 5 +- .../devlog/ui/action/ToggleSelectionAction.kt | 49 ++ 9 files changed, 758 insertions(+), 653 deletions(-) create mode 100644 src/main/kotlin/com/github/yeoli/devlog/ui/MemoDetailPanel.kt create mode 100644 src/main/kotlin/com/github/yeoli/devlog/ui/MemoInputComposer.kt create mode 100644 src/main/kotlin/com/github/yeoli/devlog/ui/NotePanel.kt create mode 100644 src/main/kotlin/com/github/yeoli/devlog/ui/SelectionStatusPanel.kt create mode 100644 src/main/kotlin/com/github/yeoli/devlog/ui/action/DeleteSelectedAction.kt rename src/main/kotlin/com/github/yeoli/devlog/ui/{ => action}/MemoExportAction.kt (97%) create mode 100644 src/main/kotlin/com/github/yeoli/devlog/ui/action/ToggleSelectionAction.kt diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/DevLogPanel.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/DevLogPanel.kt index 041fbde..e439fd1 100644 --- a/src/main/kotlin/com/github/yeoli/devlog/ui/DevLogPanel.kt +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/DevLogPanel.kt @@ -5,9 +5,10 @@ import com.github.yeoli.devlog.domain.memo.service.MemoService import com.github.yeoli.devlog.domain.note.service.NoteService import com.github.yeoli.devlog.event.MemoChangedEvent import com.github.yeoli.devlog.event.MemoListener +import com.github.yeoli.devlog.ui.action.DeleteSelectedAction +import com.github.yeoli.devlog.ui.action.MemoExportAction +import com.github.yeoli.devlog.ui.action.ToggleSelectionAction import com.intellij.icons.AllIcons -import com.intellij.notification.NotificationGroupManager -import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.* import com.intellij.openapi.editor.Editor @@ -22,13 +23,12 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Key import com.intellij.openapi.wm.StatusBar import com.intellij.testFramework.LightVirtualFile -import com.intellij.ui.DocumentAdapter import com.intellij.ui.JBColor import com.intellij.ui.JBSplitter import com.intellij.ui.components.JBLabel @@ -36,15 +36,12 @@ import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBTextArea import com.intellij.util.ui.JBUI -import java.awt.* -import java.awt.event.ActionEvent -import java.awt.event.KeyAdapter -import java.awt.event.KeyEvent -import java.time.LocalDateTime +import java.awt.BorderLayout +import java.awt.CardLayout +import java.awt.Color import java.time.LocalTime import java.time.format.DateTimeFormatter -import javax.swing.* -import javax.swing.text.JTextComponent +import javax.swing.JComponent /** * Retrospect DevLog UI의 메인 컨테이너. @@ -58,32 +55,30 @@ internal class DevLogPanel( private val memoService = project.getService(MemoService::class.java) private val noteService = project.getService(NoteService::class.java) - // 밝은/어두운 테마 모두에서 사용할 색상 팔레트. private val palette = UiPalette() - // 하단 작성 영역(코멘트 입력 + Save 버튼). - private val composer = RetrospectComposer(palette, ::handleSaveRequest) + // 작성기 영역 (분리 후보) + private val composer = MemoInputComposer(palette, ::handleSaveRequest) - // 공유 메모 탭에서 사용되는 텍스트 영역. - private val sharedNotes = SharedNotesPanel(palette, ::handleSharedNotesSave) + // 공유 노트 영역 (분리 후보) + private val sharedNotes = NotePanel(palette, ::handleSharedNotesSave) - // 에디터 선택 상태를 좌측 배너로 보여주는 컴포넌트. + // 선택 상태 배너 (분리 후보) private val selectionStatusPanel = SelectionStatusPanel(palette) - // 카드 전환으로 띄우는 상세 로그 편집기. - private val recordDetailPanel = RecordDetailPanel( + private val selectedRecordIds = linkedSetOf() + + // 상세 편집기 (분리 후보) + private val recordDetailPanel = MemoDetailPanel( palette = palette, onRequestSave = ::handleDetailSave, onRequestBack = ::handleDetailBack ) - // 타임라인에서 체크된 레코드들의 ID 저장소. - private val selectedRecordIds = linkedSetOf() - - // DevLog들을 시간순으로 보여주고 조작하는 메인 뷰. - private val timeline = RetrospectTimelineView( + // 타임라인 뷰 (분리 후보: MemoListView) + private val timeline = MemoListView( palette, - RetrospectTimelineView.Interactions( + MemoListView.Interactions( onEdit = { record, index -> openEditDialog(record) }, onSnapshot = { openSnapshotInEditor(it) }, onOpenDetail = { record -> openRecordDetail(record) }, @@ -139,49 +134,9 @@ internal class DevLogPanel( ) } - /** - * 스토리지의 최신 데이터를 타임라인/툴바 상태에 반영한다. - */ - private fun refreshMemos() { - val memos = memoService.getAllMemos() - totalRecordCount = memos.size - timeline.render(memos) - refreshToolbarActions() - } - - /** - * 타임라인(위)과 작성기(아래)를 포함하는 메인 분할 레이아웃을 구성한다. - */ - private fun createMainView(): JComponent { - val timelineStack = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - // DevLog 목록 + 현재 에디터 선택 상태 배너를 하나의 패널로 묶는다. - add(timeline.component, BorderLayout.CENTER) - add(selectionStatusPanel.component, BorderLayout.SOUTH) - } - val lowerPanel = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - // 선택한 레코드와 연결하려는 context 영역과 작성기 패널이 붙어 있다. -// add(linkContextPanel.component, BorderLayout.NORTH) - add(composer.component, BorderLayout.CENTER) - } - val splitter = JBSplitter(true, 0.8f).apply { - dividerWidth = 5 - isOpaque = false - // 상단에 타임라인, 하단에 작성기를 배치하고 상단에 비중을 둔다. - firstComponent = timelineStack - secondComponent = lowerPanel - setHonorComponentsMinimumSize(true) - } - return splitter - } - - /** - * 카드 전환/선택/내보내기 등 액션이 모인 상단 툴바를 만든다. - */ + // ---------- UI 빌더 ---------- private fun createNavigationBar(): JComponent { val group = DefaultActionGroup().apply { - // 보기 전환 + 선택 토글 + 내보내기/삭제 액션을 순서대로 배치. add(viewToggleAction) add(selectionToggleAction) add(exportSelectedAction) @@ -191,7 +146,6 @@ internal class DevLogPanel( .createActionToolbar("RetrospectNavToolbar", group, true) .apply { targetComponent = null - // ToolWindow 배경과 자연스럽게 섞이도록 여백/색상을 정리. component.background = palette.editorBg component.border = JBUI.Borders.empty() } @@ -203,9 +157,25 @@ internal class DevLogPanel( } } - /** - * 카드 레이아웃을 전환하고, 액션 활성화 상태를 재계산한다. - */ + private fun createMainView(): JComponent { + val timelineStack = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + add(timeline.component, BorderLayout.CENTER) + add(selectionStatusPanel.component, BorderLayout.SOUTH) + } + val lowerPanel = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + add(composer.component, BorderLayout.CENTER) + } + return JBSplitter(true, 0.8f).apply { + dividerWidth = 5 + isOpaque = false + firstComponent = timelineStack + secondComponent = lowerPanel + setHonorComponentsMinimumSize(true) + } + } + private fun switchCard(card: String) { if (currentCard == card) return currentCard = card @@ -213,84 +183,32 @@ internal class DevLogPanel( refreshToolbarActions() } - private fun loadSharedNotes() { - sharedNotes.setContent(noteService.getNote().content) - } - - private fun openRecordDetail(record: Memo) { - recordDetailPanel.displayRecord(record) - switchCard(RECORD_DETAIL_CARD) - } - - private fun handleDetailSave(memoId: Long, updatedContent: String) { - val memo = memoService.findMemoById(memoId) - if (memo == null || memo.content == updatedContent) return - memoService.updateMemo(memoId, updatedContent) - notifyChange() - } - - private fun handleDetailBack() { - switchCard(RETROSPECTS_CARD) + // ---------- 데이터 동기화 ---------- + private fun refreshMemos() { + val memos = memoService.getAllMemos() + totalRecordCount = memos.size + timeline.render(memos) + refreshToolbarActions() } - /** - * 에디터 선택/포커스 변화 이벤트를 구독해 상태 배너를 갱신한다. - */ - private fun setupSelectionTracking(parentDisposable: Disposable) { - EditorFactory.getInstance().eventMulticaster.addSelectionListener(object : - SelectionListener { - override fun selectionChanged(e: SelectionEvent) { - if (e.editor.project != project) return - // 에디터 선택 길이/파일명 변화를 즉시 배너에 반영. - updateSelectionBanner() - } - }, parentDisposable) - - project.messageBus.connect(parentDisposable).subscribe( - FileEditorManagerListener.FILE_EDITOR_MANAGER, - object : FileEditorManagerListener { - override fun selectionChanged(event: FileEditorManagerEvent) { - // 다른 에디터로 전환되면 선택 상태가 달라질 수 있으므로 갱신. - updateSelectionBanner() - } - } - ) - - updateSelectionBanner() + private fun loadSharedNotes() { + sharedNotes.setContent(noteService.getNote().content) } - /** - * 현재 에디터의 선택 영역 유무에 따라 배너 문구를 바꾼다. - */ - private fun updateSelectionBanner() { - val editor = FileEditorManager.getInstance(project).selectedTextEditor - val selectionModel = editor?.selectionModel - val hasMeaningfulSelection = selectionModel?.hasSelection() == true && - selectionModel.selectionStart != selectionModel.selectionEnd - if (hasMeaningfulSelection) { - // 파일명이 없으면 현재 파일이라는 문구로 대체. - val fileName = editor?.virtualFile?.name?.takeIf { it.isNotBlank() } ?: "current file" - selectionStatusPanel.showSelectionActive(fileName) - } else { - selectionStatusPanel.showIdleState() - } + private fun notifyChange() { + project.messageBus.syncPublisher(MemoChangedEvent.TOPIC).onChanged() } - /** - * 작성기의 Save 트리거를 처리한다. 선택 영역이 있으면 Draft로, 아니면 일반 로그로 저장한다. - */ - // 저장되는 로직 + // ---------- 메모/노트 액션 ---------- private fun handleSaveRequest(rawBody: String) { val body = rawBody.trim() if (body.isEmpty()) { composer.showEmptyBodyMessage() return } - val memo = memoService.createMemo(rawBody) - if (memo == null) return + val memo = memoService.createMemo(rawBody) ?: return memoService.saveMemo(memo) notifyChange() - composer.clear() composer.updateStatus("Saved at ${currentTimeString()}") } @@ -301,53 +219,20 @@ internal class DevLogPanel( sharedNotes.markSaved(currentTimeString()) } - private fun notifyChange() { - project.messageBus.syncPublisher(MemoChangedEvent.TOPIC).onChanged() - } - - private fun handleSelectionChanged(ids: Set) { - selectedRecordIds.clear() - selectedRecordIds.addAll(ids) - refreshToolbarActions() - } - - private fun getSelectedRecords(): List { - if (selectedRecordIds.isEmpty()) return emptyList() - val selected = selectedRecordIds.toSet() - return memoService.getAllMemos() - .sortedBy { it.createdAt }.filter { it.id in selected } + private fun openRecordDetail(record: Memo) { + recordDetailPanel.displayRecord(record) + switchCard(RECORD_DETAIL_CARD) } - private fun deleteSelectedRecords(records: List) { - if (records.isEmpty()) return - val ids = records.map { it.id }.toSet() - - // memoService에 removeMemosByIds로 바꾸면 될듯 - if (ids.isEmpty()) return - val memos = memoService.getAllMemos().filter { ids.contains(it.id) } - memoService.removeMemos(memos) - + private fun handleDetailSave(memoId: Long, updatedContent: String) { + val memo = memoService.findMemoById(memoId) + if (memo == null || memo.content == updatedContent) return + memoService.updateMemo(memoId, updatedContent) notifyChange() - selectedRecordIds.removeAll(ids) - refreshToolbarActions() - } - - private fun selectAllRecords() { - timeline.selectAllRecords() } - private fun clearSelection() { - timeline.clearSelection() - } - - private fun refreshToolbarActions() { - val timelineActive = currentCard == RETROSPECTS_CARD - // 타임라인이 아닐 때는 선택 관련 컨트롤을 비활성화한다. - selectionToggleAction.setControlsEnabled(timelineActive) - selectionToggleAction.updateState(totalRecordCount, selectedRecordIds.size) - exportSelectedAction.setForceDisabled(!timelineActive) - deleteSelectedAction.setForceDisabled(!timelineActive) - navigationToolbar?.updateActionsImmediately() + private fun handleDetailBack() { + switchCard(RETROSPECTS_CARD) } private fun openEditDialog(memo: Memo) { @@ -361,7 +246,7 @@ internal class DevLogPanel( add(JBLabel("Update content:"), BorderLayout.NORTH) add(JBScrollPane(editorArea), BorderLayout.CENTER) } - val dialog = object : com.intellij.openapi.ui.DialogWrapper(project) { + val dialog = object : DialogWrapper(project) { init { title = "Edit Memo" init() @@ -391,6 +276,83 @@ internal class DevLogPanel( } } + private fun deleteSelectedRecords(records: List) { + if (records.isEmpty()) return + val ids = records.map { it.id }.toSet() + if (ids.isEmpty()) return + val memos = memoService.getAllMemos().filter { ids.contains(it.id) } + memoService.removeMemos(memos) + notifyChange() + selectedRecordIds.removeAll(ids) + refreshToolbarActions() + } + + private fun getSelectedRecords(): List { + if (selectedRecordIds.isEmpty()) return emptyList() + val selected = selectedRecordIds.toSet() + return memoService.getAllMemos() + .sortedBy { it.createdAt }.filter { it.id in selected } + } + + // ---------- 선택/툴바 상태 ---------- + private fun handleSelectionChanged(ids: Set) { + selectedRecordIds.clear() + selectedRecordIds.addAll(ids) + refreshToolbarActions() + } + + private fun selectAllRecords() { + timeline.selectAllRecords() + } + + private fun clearSelection() { + timeline.clearSelection() + } + + private fun refreshToolbarActions() { + val timelineActive = currentCard == RETROSPECTS_CARD + selectionToggleAction.setControlsEnabled(timelineActive) + selectionToggleAction.updateState(totalRecordCount, selectedRecordIds.size) + exportSelectedAction.setForceDisabled(!timelineActive) + deleteSelectedAction.setForceDisabled(!timelineActive) + navigationToolbar?.updateActionsImmediately() + } + + private fun setupSelectionTracking(parentDisposable: Disposable) { + EditorFactory.getInstance().eventMulticaster.addSelectionListener(object : + SelectionListener { + override fun selectionChanged(e: SelectionEvent) { + if (e.editor.project != project) return + updateSelectionBanner() + } + }, parentDisposable) + + project.messageBus.connect(parentDisposable).subscribe( + FileEditorManagerListener.FILE_EDITOR_MANAGER, + object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + updateSelectionBanner() + } + } + ) + + updateSelectionBanner() + } + + private fun updateSelectionBanner() { + val editor = FileEditorManager.getInstance(project).selectedTextEditor + val selectionModel = editor?.selectionModel + val hasMeaningfulSelection = selectionModel?.hasSelection() == true && + selectionModel.selectionStart != selectionModel.selectionEnd + if (hasMeaningfulSelection) { + val fileName = editor?.virtualFile?.name?.takeIf { it.isNotBlank() } ?: "current file" + selectionStatusPanel.showSelectionActive(fileName) + } else { + selectionStatusPanel.showIdleState() + } + } + + // ---------- 카드 전환 액션 ---------- private inner class SharedNotesToggleAction : ToggleAction("Memos", "Toggle Memo List / Notes", AllIcons.General.History) { @@ -525,476 +487,3 @@ data class UiPalette( val listRowBg: JBColor = JBColor(Color(0xF7, 0xF8, 0xFA), Color(0x2B, 0x2D, 0x30)), val listRowSelectedBg: JBColor = JBColor(Color(0xE3, 0xF2, 0xFD), Color(0x1F, 0x3B, 0x4D)) ) - -private class RetrospectComposer( - private val palette: UiPalette, - private val onRequestSave: (String) -> Unit -) { - private val descriptionArea = object : JBTextArea(6, 40) { - override fun getLocationOnScreen(): Point = - if (isShowing) super.getLocationOnScreen() else Point(0, 0) - }.apply { - lineWrap = true - wrapStyleWord = true - border = JBUI.Borders.empty(8, 12, 8, 12) - background = palette.editorBg - foreground = palette.editorFg - caretColor = palette.editorFg - font = font.deriveFont(14f) - emptyText.text = "Write log entry..." - } - private val statusLabel = JBLabel().apply { - foreground = JBColor.gray - } - private val saveButton = JButton("Save Log").apply { - isEnabled = false - background = palette.editorBg - foreground = palette.editorFg - addActionListener { onRequestSave(descriptionArea.text) } - } - - val component: JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - foreground = palette.editorFg - border = JBUI.Borders.empty() - add(descriptionArea, BorderLayout.CENTER) - add(createControls(), BorderLayout.SOUTH) - } - - init { - descriptionArea.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(e: javax.swing.event.DocumentEvent) { - val hasText = descriptionArea.text.isNotBlank() - saveButton.isEnabled = hasText - statusLabel.text = if (hasText) "Unsaved changes" else "" - } - }) - registerSaveShortcut(descriptionArea) { - if (saveButton.isEnabled) { - saveButton.doClick() - } - } - } - - private fun createControls(): JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - foreground = palette.editorFg - border = JBUI.Borders.empty(0, 12, 8, 12) - add(statusLabel, BorderLayout.WEST) - val buttonPanel = JBPanel>(FlowLayout(FlowLayout.RIGHT, 0, 0)).apply { - background = palette.editorBg - foreground = palette.editorFg - add(saveButton) - } - add(buttonPanel, BorderLayout.EAST) - } - - fun clear() { - descriptionArea.text = "" - } - - fun updateStatus(text: String) { - statusLabel.text = text - } - - fun showEmptyBodyMessage() { - statusLabel.text = "Please enter a log entry." - } - -} - -private class RecordDetailPanel( - private val palette: UiPalette, - private val onRequestSave: (Long, String) -> Unit, - private val onRequestBack: () -> Unit -) { - private val bodyArea = JBTextArea(12, 40).apply { - lineWrap = true - wrapStyleWord = true - border = JBUI.Borders.empty(8, 12, 8, 12) - background = palette.editorBg - foreground = palette.editorFg - caretColor = palette.editorFg - font = font.deriveFont(14f) - } - private val statusLabel = JBLabel("Autosave ready").apply { - foreground = JBColor.gray - } - private val titleLabel = JBLabel("").apply { - foreground = palette.editorFg - border = JBUI.Borders.emptyLeft(4) - } - private val backButton = JButton("Back").apply { - icon = AllIcons.Actions.Back - addActionListener { - triggerManualSave() - onRequestBack() - } - } - private val autoSaveTimer = Timer(AUTO_SAVE_DELAY_MS) { - performAutoSave() - }.apply { - isRepeats = false - } - private var hasUnsavedChanges = false - private var suppressEvent = false - private var currentRecordId: Long? = null - - val component: JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - foreground = palette.editorFg - add(createHeader(), BorderLayout.NORTH) - add(JBScrollPane(bodyArea).apply { - border = JBUI.Borders.empty() - background = palette.editorBg - viewport.background = palette.editorBg - }, BorderLayout.CENTER) - add(createFooter(), BorderLayout.SOUTH) - } - - init { - bodyArea.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(e: javax.swing.event.DocumentEvent) { - if (suppressEvent) return - hasUnsavedChanges = true - statusLabel.text = "Auto-saving..." - scheduleAutoSave() - } - }) - registerSaveShortcut(bodyArea) { - triggerManualSave() - } - } - - fun displayRecord(record: Memo) { - currentRecordId = record.id - titleLabel.text = buildTitle(record) - suppressEvent = true - bodyArea.text = record.content - bodyArea.caretPosition = bodyArea.text.length - suppressEvent = false - hasUnsavedChanges = false - cancelAutoSave() - statusLabel.text = "Autosave ready" - bodyArea.requestFocusInWindow() - } - - private fun createHeader(): JComponent = - JBPanel>(BorderLayout()).apply { - background = palette.editorBg - border = JBUI.Borders.empty(8, 12, 4, 12) - val leftGroup = JBPanel>(FlowLayout(FlowLayout.LEFT, 8, 0)).apply { - background = palette.editorBg - add(backButton) - add(titleLabel) - } - add(leftGroup, BorderLayout.WEST) - } - - private fun createFooter(): JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - border = JBUI.Borders.empty(8, 12, 12, 12) - add(statusLabel, BorderLayout.WEST) - } - - private fun buildTitle(record: Memo): String { - val fileSegment = record.filePath - ?.substringAfterLast('/') - ?.takeIf { it.isNotBlank() } - ?: "General Log" - val formattedTime = runCatching { LocalDateTime.parse(record.createdAt.toString()) } - .getOrNull() - ?.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) - ?: record.createdAt.toString() - return "$fileSegment · $formattedTime" - } - - private fun scheduleAutoSave() { - if (!hasUnsavedChanges) return - autoSaveTimer.restart() - } - - private fun cancelAutoSave() { - if (autoSaveTimer.isRunning) { - autoSaveTimer.stop() - } - } - - private fun performAutoSave() { - if (!hasUnsavedChanges) return - val recordId = currentRecordId ?: return - hasUnsavedChanges = false - onRequestSave(recordId, bodyArea.text) - statusLabel.text = "Saved" - } - - private fun triggerManualSave() { - if (!hasUnsavedChanges) return - cancelAutoSave() - performAutoSave() - } - - companion object { - private const val AUTO_SAVE_DELAY_MS = 1500 - } -} - -private class SharedNotesPanel( - private val palette: UiPalette, - private val onRequestSave: (String) -> Unit -) { - private val autoSaveTimer = Timer(AUTO_SAVE_DELAY_MS) { - performAutoSave() - }.apply { - isRepeats = false - } - private val notesArea = JBTextArea(6, 40).apply { - lineWrap = true - wrapStyleWord = true - border = JBUI.Borders.empty(8, 12, 8, 12) - background = palette.editorBg - foreground = palette.editorFg - caretColor = palette.editorFg - } - private val statusLabel = JBLabel("Saved").apply { - foreground = JBColor.gray - } - private var hasUnsavedChanges = false - private var suppressEvent = false - - val component: JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - foreground = palette.editorFg - border = JBUI.Borders.empty(12, 0, 0, 0) - val header = JBLabel("Notes").apply { - border = JBUI.Borders.empty(0, 12, 8, 12) - } - val scrollPane = JBScrollPane(notesArea).apply { - border = JBUI.Borders.empty() - background = palette.editorBg - viewport.background = palette.editorBg - } - add(header, BorderLayout.NORTH) - add(scrollPane, BorderLayout.CENTER) - add(createControls(), BorderLayout.SOUTH) - } - - init { - notesArea.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(e: javax.swing.event.DocumentEvent) { - if (suppressEvent) return - hasUnsavedChanges = true - statusLabel.text = "Saving..." - scheduleAutoSave() - } - }) - registerSaveShortcut(notesArea) { - triggerManualSave() - } - } - - private fun createControls(): JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - border = JBUI.Borders.empty(0, 12, 12, 12) - add(statusLabel, BorderLayout.WEST) - } - - fun setContent(value: String) { - suppressEvent = true - notesArea.text = value - suppressEvent = false - hasUnsavedChanges = false - cancelAutoSave() - statusLabel.text = "Saved" - } - - fun markSaved(timestamp: String) { - cancelAutoSave() - hasUnsavedChanges = false - statusLabel.text = "Saved at $timestamp" - } - - private fun scheduleAutoSave() { - if (!hasUnsavedChanges) return - autoSaveTimer.restart() - } - - private fun cancelAutoSave() { - if (autoSaveTimer.isRunning) { - autoSaveTimer.stop() - } - } - - private fun performAutoSave() { - if (!hasUnsavedChanges) return - statusLabel.text = "Auto-saving..." - hasUnsavedChanges = false - onRequestSave(notesArea.text) - } - - private fun triggerManualSave() { - if (!hasUnsavedChanges) return - cancelAutoSave() - performAutoSave() - } - - companion object { - private const val AUTO_SAVE_DELAY_MS = 2000 - } -} - -private class SelectionStatusPanel( - private val palette: UiPalette -) { - private val infoLabel = JBLabel().apply { - foreground = JBColor.gray - font = font.deriveFont(font.size2D - 1f) - horizontalAlignment = JBLabel.LEFT - verticalAlignment = JBLabel.CENTER - } - - val component: JComponent = JBPanel>(BorderLayout()).apply { - background = palette.editorBg - foreground = palette.editorFg - border = JBUI.Borders.empty(4, 12, 4, 12) - add(infoLabel, BorderLayout.CENTER) - val fixedHeight = 30 - preferredSize = JBUI.size(0, fixedHeight) - minimumSize = JBUI.size(0, fixedHeight) - } - - init { - showIdleState() - } - - fun showSelectionActive(fileName: String) { - infoLabel.text = "Selected $fileName" - infoLabel.foreground = JBColor.gray - } - - fun showIdleState() { - infoLabel.text = "No code selected." - infoLabel.foreground = JBColor.gray - } -} - -private fun registerSaveShortcut(component: JComponent, action: () -> Unit) { - if (GraphicsEnvironment.isHeadless()) return - val shortcutMask = Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx - val actionKey = "devlog.saveShortcut.${component.hashCode()}" - val keyStrokes = listOf( - KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask), - KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask or KeyEvent.SHIFT_DOWN_MASK) - ) - keyStrokes.forEach { keyStroke -> - component.getInputMap(JComponent.WHEN_FOCUSED).put(keyStroke, actionKey) - component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, actionKey) - } - component.actionMap.put(actionKey, object : AbstractAction() { - override fun actionPerformed(e: ActionEvent?) { - action() - } - }) - if (component is JTextComponent) { - component.addKeyListener(object : KeyAdapter() { - override fun keyPressed(e: KeyEvent) { - val metaCombo = e.isMetaDown || e.isControlDown - if (metaCombo && e.keyCode == KeyEvent.VK_ENTER) { - e.consume() - action() - } - } - }) - } -} - -private class DeleteSelectedAction( - private val project: Project, - private val selectionProvider: () -> List, - private val onDelete: (List) -> Unit -) : DumbAwareAction( - "Delete Selected Logs", - "Delete all checked DevLog entries", - AllIcons.Actions.GC -) { - - private var forceDisabled = false - - fun setForceDisabled(disabled: Boolean) { - forceDisabled = disabled - } - - override fun actionPerformed(e: AnActionEvent) { - if (forceDisabled) return - val selected = selectionProvider() - if (selected.isEmpty()) { - NotificationGroupManager.getInstance() - .getNotificationGroup("YeoliRetrospectNotifications") - ?.createNotification( - "Delete Selected Logs", - "No DevLog entries are selected.", - NotificationType.WARNING - ) - ?.notify(project) - return - } - val confirmed = Messages.showYesNoDialog( - project, - "Delete ${selected.size} selected DevLog entries?", - "Delete Selected Logs", - Messages.getQuestionIcon() - ) - if (confirmed == Messages.YES) { - onDelete(selected) - } - } - - override fun update(e: AnActionEvent) { - super.update(e) - e.presentation.isEnabled = !forceDisabled && selectionProvider().isNotEmpty() - } -} - -private class ToggleSelectionAction( - private val onSelectAll: () -> Unit, - private val onClearSelection: () -> Unit -) : DumbAwareAction("Select All Logs", "Select every DevLog entry", AllIcons.Actions.Selectall) { - - private var totalItems: Int = 0 - private var selectedItems: Int = 0 - private var controlsEnabled: Boolean = true - - fun updateState(total: Int, selected: Int) { - totalItems = total - selectedItems = selected - } - - fun setControlsEnabled(enabled: Boolean) { - controlsEnabled = enabled - } - - override fun actionPerformed(e: AnActionEvent) { - if (!controlsEnabled || totalItems == 0) return - if (selectedItems == totalItems) { - onClearSelection() - } else { - onSelectAll() - } - } - - override fun update(e: AnActionEvent) { - super.update(e) - val hasRecords = totalItems > 0 - val allSelected = hasRecords && selectedItems == totalItems - e.presentation.isEnabled = controlsEnabled && hasRecords - if (allSelected) { - e.presentation.text = "Clear Selection" - e.presentation.description = "Clear all DevLog selections" - e.presentation.icon = AllIcons.Actions.Unselectall - } else { - e.presentation.text = "Select All" - e.presentation.description = "Select every DevLog entry" - e.presentation.icon = AllIcons.Actions.Selectall - } - } -} diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/MemoDetailPanel.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/MemoDetailPanel.kt new file mode 100644 index 0000000..c1734b7 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/MemoDetailPanel.kt @@ -0,0 +1,188 @@ +package com.github.yeoli.devlog.ui + +import com.github.yeoli.devlog.domain.memo.domain.Memo +import com.intellij.icons.AllIcons +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import java.awt.FlowLayout +import java.awt.GraphicsEnvironment +import java.awt.Toolkit +import java.awt.event.ActionEvent +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import javax.swing.* +import javax.swing.text.JTextComponent + +class MemoDetailPanel( + private val palette: UiPalette, + private val onRequestSave: (Long, String) -> Unit, + private val onRequestBack: () -> Unit +) { + private val bodyArea = JBTextArea(12, 40).apply { + lineWrap = true + wrapStyleWord = true + border = JBUI.Borders.empty(8, 12, 8, 12) + background = palette.editorBg + foreground = palette.editorFg + caretColor = palette.editorFg + font = font.deriveFont(14f) + } + private val statusLabel = JBLabel("Autosave ready").apply { + foreground = JBColor.gray + } + private val titleLabel = JBLabel("").apply { + foreground = palette.editorFg + border = JBUI.Borders.emptyLeft(4) + } + private val backButton = JButton("Back").apply { + icon = AllIcons.Actions.Back + addActionListener { + triggerManualSave() + onRequestBack() + } + } + private val autoSaveTimer = Timer(AUTO_SAVE_DELAY_MS) { + performAutoSave() + }.apply { + isRepeats = false + } + private var hasUnsavedChanges = false + private var suppressEvent = false + private var currentRecordId: Long? = null + + val component: JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + foreground = palette.editorFg + add(createHeader(), BorderLayout.NORTH) + add(JBScrollPane(bodyArea).apply { + border = JBUI.Borders.empty() + background = palette.editorBg + viewport.background = palette.editorBg + }, BorderLayout.CENTER) + add(createFooter(), BorderLayout.SOUTH) + } + + init { + bodyArea.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: javax.swing.event.DocumentEvent) { + if (suppressEvent) return + hasUnsavedChanges = true + statusLabel.text = "Auto-saving..." + scheduleAutoSave() + } + }) + registerSaveShortcut(bodyArea) { + triggerManualSave() + } + } + + fun displayRecord(record: Memo) { + currentRecordId = record.id + titleLabel.text = buildTitle(record) + suppressEvent = true + bodyArea.text = record.content + bodyArea.caretPosition = bodyArea.text.length + suppressEvent = false + hasUnsavedChanges = false + cancelAutoSave() + statusLabel.text = "Autosave ready" + bodyArea.requestFocusInWindow() + } + + private fun createHeader(): JComponent = + JBPanel>(BorderLayout()).apply { + background = palette.editorBg + border = JBUI.Borders.empty(8, 12, 4, 12) + val leftGroup = JBPanel>(FlowLayout(FlowLayout.LEFT, 8, 0)).apply { + background = palette.editorBg + add(backButton) + add(titleLabel) + } + add(leftGroup, BorderLayout.WEST) + } + + private fun createFooter(): JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + border = JBUI.Borders.empty(8, 12, 12, 12) + add(statusLabel, BorderLayout.WEST) + } + + private fun buildTitle(record: Memo): String { + val fileSegment = record.filePath + ?.substringAfterLast('/') + ?.takeIf { it.isNotBlank() } + ?: "General Log" + val formattedTime = runCatching { LocalDateTime.parse(record.createdAt.toString()) } + .getOrNull() + ?.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + ?: record.createdAt.toString() + return "$fileSegment · $formattedTime" + } + + private fun scheduleAutoSave() { + if (!hasUnsavedChanges) return + autoSaveTimer.restart() + } + + private fun cancelAutoSave() { + if (autoSaveTimer.isRunning) { + autoSaveTimer.stop() + } + } + + private fun performAutoSave() { + if (!hasUnsavedChanges) return + val recordId = currentRecordId ?: return + hasUnsavedChanges = false + onRequestSave(recordId, bodyArea.text) + statusLabel.text = "Saved" + } + + private fun triggerManualSave() { + if (!hasUnsavedChanges) return + cancelAutoSave() + performAutoSave() + } + + companion object { + private const val AUTO_SAVE_DELAY_MS = 1500 + } +} + +private fun registerSaveShortcut(component: JComponent, action: () -> Unit) { + if (GraphicsEnvironment.isHeadless()) return + val shortcutMask = Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx + val actionKey = "devlog.saveShortcut.${component.hashCode()}" + val keyStrokes = listOf( + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask), + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask or KeyEvent.SHIFT_DOWN_MASK) + ) + keyStrokes.forEach { keyStroke -> + component.getInputMap(JComponent.WHEN_FOCUSED).put(keyStroke, actionKey) + component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, actionKey) + } + component.actionMap.put(actionKey, object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + action() + } + }) + if (component is JTextComponent) { + component.addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + val metaCombo = e.isMetaDown || e.isControlDown + if (metaCombo && e.keyCode == KeyEvent.VK_ENTER) { + e.consume() + action() + } + } + }) + } +} diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/MemoInputComposer.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/MemoInputComposer.kt new file mode 100644 index 0000000..40c9979 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/MemoInputComposer.kt @@ -0,0 +1,124 @@ +package com.github.yeoli.devlog.ui + +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.JBUI +import java.awt.* +import java.awt.event.ActionEvent +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import javax.swing.AbstractAction +import javax.swing.JButton +import javax.swing.JComponent +import javax.swing.KeyStroke +import javax.swing.text.JTextComponent + +class MemoInputComposer( + private val palette: UiPalette, + private val onRequestSave: (String) -> Unit +) { + private val descriptionArea = object : JBTextArea(6, 40) { + override fun getLocationOnScreen(): Point = + if (isShowing) super.getLocationOnScreen() else Point(0, 0) + }.apply { + lineWrap = true + wrapStyleWord = true + border = JBUI.Borders.empty(8, 12, 8, 12) + background = palette.editorBg + foreground = palette.editorFg + caretColor = palette.editorFg + font = font.deriveFont(14f) + emptyText.text = "Write log entry..." + } + private val statusLabel = JBLabel().apply { + foreground = JBColor.gray + } + private val saveButton = JButton("Save Log").apply { + isEnabled = false + background = palette.editorBg + foreground = palette.editorFg + addActionListener { onRequestSave(descriptionArea.text) } + } + + val component: JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + foreground = palette.editorFg + border = JBUI.Borders.empty() + add(descriptionArea, BorderLayout.CENTER) + add(createControls(), BorderLayout.SOUTH) + } + + init { + descriptionArea.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: javax.swing.event.DocumentEvent) { + val hasText = descriptionArea.text.isNotBlank() + saveButton.isEnabled = hasText + statusLabel.text = if (hasText) "Unsaved changes" else "" + } + }) + registerSaveShortcut(descriptionArea) { + if (saveButton.isEnabled) { + saveButton.doClick() + } + } + } + + private fun createControls(): JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + foreground = palette.editorFg + border = JBUI.Borders.empty(0, 12, 8, 12) + add(statusLabel, BorderLayout.WEST) + val buttonPanel = JBPanel>(FlowLayout(FlowLayout.RIGHT, 0, 0)).apply { + background = palette.editorBg + foreground = palette.editorFg + add(saveButton) + } + add(buttonPanel, BorderLayout.EAST) + } + + fun clear() { + descriptionArea.text = "" + } + + fun updateStatus(text: String) { + statusLabel.text = text + } + + fun showEmptyBodyMessage() { + statusLabel.text = "Please enter a log entry." + } + +} + +private fun registerSaveShortcut(component: JComponent, action: () -> Unit) { + if (GraphicsEnvironment.isHeadless()) return + val shortcutMask = Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx + val actionKey = "devlog.saveShortcut.${component.hashCode()}" + val keyStrokes = listOf( + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask), + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask or KeyEvent.SHIFT_DOWN_MASK) + ) + keyStrokes.forEach { keyStroke -> + component.getInputMap(JComponent.WHEN_FOCUSED).put(keyStroke, actionKey) + component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, actionKey) + } + component.actionMap.put(actionKey, object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + action() + } + }) + if (component is JTextComponent) { + component.addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + val metaCombo = e.isMetaDown || e.isControlDown + if (metaCombo && e.keyCode == KeyEvent.VK_ENTER) { + e.consume() + action() + } + } + }) + } +} diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/MemoListView.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/MemoListView.kt index 4952889..eb8a4a0 100644 --- a/src/main/kotlin/com/github/yeoli/devlog/ui/MemoListView.kt +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/MemoListView.kt @@ -24,7 +24,7 @@ private val TITLE_MUTED_COLOR = JBColor(Color(0x88, 0x88, 0x88), Color(0xc0, 0xc private val DATE_LABEL_BORDER = JBColor(Color(0xe3, 0xe6, 0xed), Color(0x33, 0x34, 0x38)) private val DATE_LABEL_TEXT = JBColor(Color(0x4d5160), Color(0xb8bcc9)) -class RetrospectTimelineView( +class MemoListView( private val palette: UiPalette, private val interactions: Interactions ) { diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/NotePanel.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/NotePanel.kt new file mode 100644 index 0000000..2672e26 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/NotePanel.kt @@ -0,0 +1,154 @@ +package com.github.yeoli.devlog.ui + +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.JBScrollPane +import com.intellij.ui.components.JBTextArea +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import java.awt.GraphicsEnvironment +import java.awt.Toolkit +import java.awt.event.ActionEvent +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import javax.swing.AbstractAction +import javax.swing.JComponent +import javax.swing.KeyStroke +import javax.swing.Timer +import javax.swing.text.JTextComponent + +class NotePanel( + private val palette: UiPalette, + private val onRequestSave: (String) -> Unit +) { + private val autoSaveTimer = Timer(AUTO_SAVE_DELAY_MS) { + performAutoSave() + }.apply { + isRepeats = false + } + private val notesArea = JBTextArea(6, 40).apply { + lineWrap = true + wrapStyleWord = true + border = JBUI.Borders.empty(8, 12, 8, 12) + background = palette.editorBg + foreground = palette.editorFg + caretColor = palette.editorFg + } + private val statusLabel = JBLabel("Saved").apply { + foreground = JBColor.gray + } + private var hasUnsavedChanges = false + private var suppressEvent = false + + val component: JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + foreground = palette.editorFg + border = JBUI.Borders.empty(12, 0, 0, 0) + val header = JBLabel("Notes").apply { + border = JBUI.Borders.empty(0, 12, 8, 12) + } + val scrollPane = JBScrollPane(notesArea).apply { + border = JBUI.Borders.empty() + background = palette.editorBg + viewport.background = palette.editorBg + } + add(header, BorderLayout.NORTH) + add(scrollPane, BorderLayout.CENTER) + add(createControls(), BorderLayout.SOUTH) + } + + init { + notesArea.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: javax.swing.event.DocumentEvent) { + if (suppressEvent) return + hasUnsavedChanges = true + statusLabel.text = "Saving..." + scheduleAutoSave() + } + }) + registerSaveShortcut(notesArea) { + triggerManualSave() + } + } + + private fun createControls(): JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + border = JBUI.Borders.empty(0, 12, 12, 12) + add(statusLabel, BorderLayout.WEST) + } + + fun setContent(value: String) { + suppressEvent = true + notesArea.text = value + suppressEvent = false + hasUnsavedChanges = false + cancelAutoSave() + statusLabel.text = "Saved" + } + + fun markSaved(timestamp: String) { + cancelAutoSave() + hasUnsavedChanges = false + statusLabel.text = "Saved at $timestamp" + } + + private fun scheduleAutoSave() { + if (!hasUnsavedChanges) return + autoSaveTimer.restart() + } + + private fun cancelAutoSave() { + if (autoSaveTimer.isRunning) { + autoSaveTimer.stop() + } + } + + private fun performAutoSave() { + if (!hasUnsavedChanges) return + statusLabel.text = "Auto-saving..." + hasUnsavedChanges = false + onRequestSave(notesArea.text) + } + + private fun triggerManualSave() { + if (!hasUnsavedChanges) return + cancelAutoSave() + performAutoSave() + } + + companion object { + private const val AUTO_SAVE_DELAY_MS = 2000 + } +} + +private fun registerSaveShortcut(component: JComponent, action: () -> Unit) { + if (GraphicsEnvironment.isHeadless()) return + val shortcutMask = Toolkit.getDefaultToolkit().menuShortcutKeyMaskEx + val actionKey = "devlog.saveShortcut.${component.hashCode()}" + val keyStrokes = listOf( + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask), + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, shortcutMask or KeyEvent.SHIFT_DOWN_MASK) + ) + keyStrokes.forEach { keyStroke -> + component.getInputMap(JComponent.WHEN_FOCUSED).put(keyStroke, actionKey) + component.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, actionKey) + } + component.actionMap.put(actionKey, object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + action() + } + }) + if (component is JTextComponent) { + component.addKeyListener(object : KeyAdapter() { + override fun keyPressed(e: KeyEvent) { + val metaCombo = e.isMetaDown || e.isControlDown + if (metaCombo && e.keyCode == KeyEvent.VK_ENTER) { + e.consume() + action() + } + } + }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/SelectionStatusPanel.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/SelectionStatusPanel.kt new file mode 100644 index 0000000..ea589d2 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/SelectionStatusPanel.kt @@ -0,0 +1,43 @@ +package com.github.yeoli.devlog.ui + +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import javax.swing.JComponent + +class SelectionStatusPanel( + private val palette: UiPalette +) { + private val infoLabel = JBLabel().apply { + foreground = JBColor.gray + font = font.deriveFont(font.size2D - 1f) + horizontalAlignment = JBLabel.LEFT + verticalAlignment = JBLabel.CENTER + } + + val component: JComponent = JBPanel>(BorderLayout()).apply { + background = palette.editorBg + foreground = palette.editorFg + border = JBUI.Borders.empty(4, 12, 4, 12) + add(infoLabel, BorderLayout.CENTER) + val fixedHeight = 30 + preferredSize = JBUI.size(0, fixedHeight) + minimumSize = JBUI.size(0, fixedHeight) + } + + init { + showIdleState() + } + + fun showSelectionActive(fileName: String) { + infoLabel.text = "Selected $fileName" + infoLabel.foreground = JBColor.gray + } + + fun showIdleState() { + infoLabel.text = "No code selected." + infoLabel.foreground = JBColor.gray + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/action/DeleteSelectedAction.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/action/DeleteSelectedAction.kt new file mode 100644 index 0000000..3be3d68 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/action/DeleteSelectedAction.kt @@ -0,0 +1,57 @@ +package com.github.yeoli.devlog.ui.action + +import com.github.yeoli.devlog.domain.memo.domain.Memo +import com.intellij.icons.AllIcons +import com.intellij.notification.NotificationGroupManager +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages + +class DeleteSelectedAction( + private val project: Project, + private val selectionProvider: () -> List, + private val onDelete: (List) -> Unit +) : DumbAwareAction( + "Delete Selected Logs", + "Delete all checked DevLog entries", + AllIcons.Actions.GC +) { + + private var forceDisabled = false + + fun setForceDisabled(disabled: Boolean) { + forceDisabled = disabled + } + + override fun actionPerformed(e: AnActionEvent) { + if (forceDisabled) return + val selected = selectionProvider() + if (selected.isEmpty()) { + NotificationGroupManager.getInstance() + .getNotificationGroup("YeoliRetrospectNotifications") + ?.createNotification( + "Delete Selected Logs", + "No DevLog entries are selected.", + NotificationType.WARNING + ) + ?.notify(project) + return + } + val confirmed = Messages.showYesNoDialog( + project, + "Delete ${selected.size} selected DevLog entries?", + "Delete Selected Logs", + Messages.getQuestionIcon() + ) + if (confirmed == Messages.YES) { + onDelete(selected) + } + } + + override fun update(e: AnActionEvent) { + super.update(e) + e.presentation.isEnabled = !forceDisabled && selectionProvider().isNotEmpty() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/MemoExportAction.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/action/MemoExportAction.kt similarity index 97% rename from src/main/kotlin/com/github/yeoli/devlog/ui/MemoExportAction.kt rename to src/main/kotlin/com/github/yeoli/devlog/ui/action/MemoExportAction.kt index 2d9902d..1a4f244 100644 --- a/src/main/kotlin/com/github/yeoli/devlog/ui/MemoExportAction.kt +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/action/MemoExportAction.kt @@ -1,6 +1,7 @@ -package com.github.yeoli.devlog.ui +package com.github.yeoli.devlog.ui.action import com.github.yeoli.devlog.domain.memo.domain.Memo +import com.github.yeoli.devlog.ui.MemoExportPipeline import com.intellij.icons.AllIcons import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType @@ -87,4 +88,4 @@ class MemoExportAction( companion object { private const val NOTIFICATION_GROUP_ID = "YeoliRetrospectNotifications" } -} +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/yeoli/devlog/ui/action/ToggleSelectionAction.kt b/src/main/kotlin/com/github/yeoli/devlog/ui/action/ToggleSelectionAction.kt new file mode 100644 index 0000000..642b4d1 --- /dev/null +++ b/src/main/kotlin/com/github/yeoli/devlog/ui/action/ToggleSelectionAction.kt @@ -0,0 +1,49 @@ +package com.github.yeoli.devlog.ui.action + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.DumbAwareAction + +class ToggleSelectionAction( + private val onSelectAll: () -> Unit, + private val onClearSelection: () -> Unit +) : DumbAwareAction("Select All Logs", "Select every DevLog entry", AllIcons.Actions.Selectall) { + + private var totalItems: Int = 0 + private var selectedItems: Int = 0 + private var controlsEnabled: Boolean = true + + fun updateState(total: Int, selected: Int) { + totalItems = total + selectedItems = selected + } + + fun setControlsEnabled(enabled: Boolean) { + controlsEnabled = enabled + } + + override fun actionPerformed(e: AnActionEvent) { + if (!controlsEnabled || totalItems == 0) return + if (selectedItems == totalItems) { + onClearSelection() + } else { + onSelectAll() + } + } + + override fun update(e: AnActionEvent) { + super.update(e) + val hasRecords = totalItems > 0 + val allSelected = hasRecords && selectedItems == totalItems + e.presentation.isEnabled = controlsEnabled && hasRecords + if (allSelected) { + e.presentation.text = "Clear Selection" + e.presentation.description = "Clear all DevLog selections" + e.presentation.icon = AllIcons.Actions.Unselectall + } else { + e.presentation.text = "Select All" + e.presentation.description = "Select every DevLog entry" + e.presentation.icon = AllIcons.Actions.Selectall + } + } +} \ No newline at end of file