emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
From: Visuwesh <visuweshm@gmail.com>
To: Ihor Radchenko <yantar92@posteo.net>
Cc: emacs-orgmode@gnu.org,  Po Lu <luangruo@yahoo.com>
Subject: Re: [BUG] [PATCH] Add yank-media and DND handler  [9.6.7 (9.6.7-g6eb773 @ /home/viz/lib/emacs/straight/build/org/)]
Date: Mon, 23 Oct 2023 15:42:32 +0530	[thread overview]
Message-ID: <87a5s98x67.fsf@gmail.com> (raw)
In-Reply-To: <875y2xaf6d.fsf@localhost> (Ihor Radchenko's message of "Mon, 23 Oct 2023 08:58:18 +0000")

[-- Attachment #1: Type: text/plain, Size: 1440 bytes --]

[திங்கள் அக்டோபர் 23, 2023] Ihor Radchenko wrote:

>>> Also, we might want to add a subsection describing the new customization to
>>> 17 Miscellaneous section of the manual. Otherwise, users might have
>>> difficulties discovering relevant settings to customize dnd and yank
>>> behavior.
>>
>> I have now added these to the manual with a few relevant concept
>> indices.  Please check if they are understandable as I am terrible at
>> writing documentation.  Thanks.
>
> Thanks!
> Upon reading the manual sections I have further comments on the code:
>
> 1. org-yank-image-save-type feels a bit inconsistent. What about
>    org-yank-image-save-method? "Type" sounds more like extension (png,
>    jpeg, etc)

You're right, I have now changed the name to say method instead.

> 2. org-dnd-method allows either attaching files/images or putting them
>    into a dedicated directory. Same for yanked images. But it is not the
>    case for yanked files - they are always attached, which is not
>    consistent. May we also allow dedicated directory for yanked files?
>    Or maybe simply merge the org-dnd-method customization for yank and
>    dnd together.

Hmm... yes, I now made yank-media also respect the value of
org-dnd-method.  I changed the name of org-dnd-method to
org-yank-dnd-method and similarly for the rest of the common user
options.

Here's the new patch:


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-support-for-yank-media-and-DND.patch --]
[-- Type: text/x-diff, Size: 15502 bytes --]

From c7492fc2adb72a45691124929527798bf9482c2b Mon Sep 17 00:00:00 2001
From: Visuwesh <visuweshm@gmail.com>
Date: Fri, 22 Sep 2023 20:11:41 +0530
Subject: [PATCH] Add support for yank-media and DND

* lisp/org.el (org-mode): Call the setup function for yank-media and
DND.
(org-setup-yank-dnd-handlers): Register yank-media-handler and DND
handler.
(org-yank-image-save-method, org-yank-image-file-name-function)
(org-yank-dnd-method, org-yank-dnd-default-attach-method): New
defcustoms.
(org--image-yank-media-handler, org--copied-files-yank-media-handler)
(org--dnd-rmc, org--dnd-attach-file, org--dnd-local-file-handler)
(org--dnd-xds-method, org--dnd-xds-function): Add yank-media and DND
handlers.

* doc/org-manual.org: (Drag and Drop & ~yank-media~): Describe the new
feature in the manual.

* etc/ORG-NEWS: Advertise the new features.
---
 doc/org-manual.org |  45 +++++++++
 etc/ORG-NEWS       |  24 +++++
 lisp/org.el        | 231 ++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 299 insertions(+), 1 deletion(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 3e9d42f55..7b14eb937 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -21095,6 +21095,51 @@ most recent since the mobile application searches files that were last
 pulled.  To get an updated agenda view with changes since the last
 pull, pull again.
 
+** Drag and Drop & ~yank-media~
+:PROPERTIES:
+:DESCRIPTION: Dropping and pasting files and images
+:END:
+
+#+cindex: dropping files
+#+cindex: dragging files
+#+cindex: drag and drop
+#+cindex: dnd
+#+vindex: org-yank-dnd-method
+Org mode supports drag and drop (DnD) of files.  By default, Org asks
+the user what must be done with the dropped file: attach it, insert
+=file:= link, or open the file.  Customize ~org-yank-dnd-method~ to
+set the default DnD action.
+
+When DnD method is "attach", Org mode first consults DnD metadata to
+decide the attach method.  For example, when file/files are dragged
+from a file manager, Org may attach by copying or by moving.
+
+#+vindex: org-yank-dnd-default-attach-method
+If Org cannot figure out which attachment method to use from the
+metadata, it defaults to ~org-yank-dnd-default-attach-method~ [fn::By
+default, ~org-yank-dnd-default-attach-method~ is set to nil -- use the same
+value as ~org-attach-method~ (~cp~ by default).]
+
+#+cindex: pasting files, images from clipboard
+Starting from Emacs 29, Org mode supports ~yank-media~ command to yank
+images from the clipboard and files from a file manager.
+
+#+vindex: org-yank-image-save-method
+When yanking images from clipboard, Org saves the image on disk and
+inserts the image link to Org buffer.  Images are either saved as
+attachments to heading (default) or to a globally defined directory.
+The save location is controlled by ~org-yank-image-save-method~.
+
+#+vindex: org-yank-image-file-name-function
+The yanked images are saved under automatically generated name.  You
+can customize ~org-yank-image-file-name-function~ to make Org query
+the image names or change the naming scheme.
+
+When yanking files copied from a file manager, Org respects the value
+of ~org-yank-dnd-method~.  Image files pasted this way also respect
+the value of ~org-yank-image-save-method~ when the action to perform
+is =attach=.
+
 * Hacking
 :PROPERTIES:
 :DESCRIPTION: How to hack your way around.
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 252c5a9f9..7c40ec0fe 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -596,6 +596,30 @@ return a matplotlib Figure object to plot. For output results, the
 current figure (as returned by =pyplot.gcf()=) is cleared before
 evaluation, and then plotted afterwards.
 
+*** Images and files in clipboard can be pasted
+
+Org asks the user what must be done when pasting images and files
+copied to the clipboard from a file manager using the ~yank-media~
+command.  The default action can be set by customizing
+~org-yank-dnd-method~.  The ~yank-media~ command was added in Emacs
+29.
+
+Images can be saved to a separate directory instead of being attached,
+customize ~org-yank-image-save-method~.
+
+Image filename chosen can be customized by setting
+~org-yank-image-file-name-function~ which by default autogenerates a
+filename based on the current time.
+
+*** Files and images can be attached by dropping onto Emacs
+
+By default, Org asks the user what to do with the dropped file like
+for pasted files.  The same user option ~org-yank-dnd-method~ is
+respected.
+
+Images dropped also respect the value of ~org-yank-image-save-method~
+when ~org-yank-dnd-method~ is =attach=.
+
 ** New functions and changes in function arguments
 *** =TYPES= argument in ~org-element-lineage~ can now be a symbol
 
diff --git a/lisp/org.el b/lisp/org.el
index d0b2355ea..317cd267f 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -4999,7 +4999,10 @@ The following commands are available:
   (org--set-faces-extend '(org-block-begin-line org-block-end-line)
                          org-fontify-whole-block-delimiter-line)
   (org--set-faces-extend org-level-faces org-fontify-whole-heading-line)
-  (setq-local org-mode-loading nil))
+  (setq-local org-mode-loading nil)
+
+  ;; `yank-media' handler and DND support.
+  (org-setup-yank-dnd-handlers))
 
 ;; Update `customize-package-emacs-version-alist'
 (add-to-list 'customize-package-emacs-version-alist
@@ -20254,6 +20257,232 @@ it has a `diary' type."
 		    (org-format-timestamp timestamp fmt t))
 	  (org-format-timestamp timestamp fmt (eq boundary 'end)))))))
 
+;;; Yank media handler and DND
+(defun org-setup-yank-dnd-handlers ()
+  "Setup the `yank-media' and DND handlers for buffer."
+  (setq-local dnd-protocol-alist
+              (cons '("^file:///" . org--dnd-local-file-handler)
+                    dnd-protocol-alist))
+  (when (fboundp 'yank-media-handler)
+    (yank-media-handler "image/.*" #'org--image-yank-media-handler)
+    ;; Looks like different DEs go for different handler names,
+    ;; https://larsee.com/blog/2019/05/clipboard-files/.
+    (yank-media-handler "x/special-\\(?:gnome\|KDE\|mate\\)-files"
+                        #'org--copied-files-yank-media-handler))
+  (when (boundp 'x-dnd-direct-save-function)
+    (setq-local x-dnd-direct-save-function #'org--dnd-xds-function)))
+
+(defcustom org-yank-image-save-method 'attach
+  "Method to save images yanked from clipboard and dropped to Emacs.
+It can be the symbol `attach' to add it as an attachment, or a
+directory name to copy/cut the image to that directory."
+  :group 'org
+  :package-version '(Org . "9.7")
+  :type '(choice (const :tag "Add it as attachment" attach)
+                 (directory :tag "Save it in directory"))
+  :safe (lambda (x) (eq x 'attach)))
+
+(defcustom org-yank-image-file-name-function #'org-yank-image-autogen-filename
+  "Function to generate filename for image yanked from clipboard.
+By default, this autogenerates a filename based on the current
+time.
+It is called with no arguments and should return a string without
+any extension which is used as the filename."
+  :group 'org
+  :package-version '(Org . "9.7")
+  :type '(radio (function-item :doc "Autogenerate filename"
+                               org-yank-image-autogen-filename)
+                (function-item :doc "Ask for filename"
+                               org-yank-image-read-filename)
+                function))
+
+(defun org-yank-image-autogen-filename ()
+  "Autogenerate filename for image in clipboard."
+  (format-time-string "clipboard-%Y%m%dT%H%M%S.%6N"))
+
+(defun org-yank-image-read-filename ()
+  "Read filename for image in clipboard."
+  (read-string "Basename for image file without extension: "))
+
+(declare-function org-attach-attach "org-attach" (file &optional visit-dir method))
+
+(defun org--image-yank-media-handler (mimetype data)
+  "Save image DATA of mime-type MIMETYPE and insert link at point.
+It is saved as per `org-yank-image-save-method'.  The name for the
+image is prompted and the extension is automatically added to the
+end."
+  (let* ((ext (symbol-name (mailcap-mime-type-to-extension mimetype)))
+         (iname (funcall org-yank-image-file-name-function))
+         (filename (file-name-with-extension iname ext))
+         (absname (expand-file-name
+                   filename
+                   (if (eq org-yank-image-save-method 'attach)
+                       temporary-file-directory
+                     org-yank-image-save-method)))
+         link)
+    (when (and (not (eq org-yank-image-save-method 'attach))
+               (not (file-directory-p org-yank-image-save-method)))
+      (make-directory org-yank-image-save-method t))
+    (with-temp-file absname
+      (insert data))
+    (if (null (eq org-yank-image-save-method 'attach))
+        (setq link (org-link-make-string (concat "file:" (file-relative-name absname))))
+      (require 'org-attach)
+      (org-attach-attach absname nil 'mv)
+      (setq link (org-link-make-string (concat "attachment:" filename))))
+    (insert link)))
+
+;; I cannot find a spec for this but
+;; https://indigo.re/posts/2021-12-21-clipboard-data.html and pcmanfm
+;; suggests that this is the format.
+(defun org--copied-files-yank-media-handler (_mimetype data)
+  "Handle copied or cut files from file manager.
+They are handled as per `org-yank-dnd-method'.
+DATA is a string where the first line is the operation to
+perform: copy or cut.  Rest of the lines are file: links to the
+concerned files."
+  ;; pcmanfm adds a null byte at the end for some reason.
+  (let* ((data (split-string data "[\0\n\r]" t))
+         (files (cdr data))
+         (action (if (equal (car data) "cut")
+                     'copy
+                   'move)))
+    (dolist (f files)
+      (if (file-readable-p f)
+          (org--dnd-local-file-handler f action)
+        (message "File `%s' is not readable, skipping" f)))))
+
+(defcustom org-yank-dnd-method 'ask
+  "Action to perform on the dropped and the pasted files.
+When the value is the symbol,
+  . `attach' -- attach dropped/pasted file
+  . `open' -- visit/open dropped/pasted file in Emacs
+  . `file-link' -- insert file: link to dropped/pasted file
+  . `ask' -- ask what to do out of the above."
+  :group 'org
+  :package-version '(Org . "9.7")
+  :type '(choice (const :tag "Attach" attach)
+                 (const :tag "Open/Visit file" open)
+                 (const :tag "Insert file: link" file-link)
+                 (const :tag "Ask what to do" ask)))
+
+(defcustom org-yank-dnd-default-attach-method nil
+  "Default attach method to use when DND action is unspecified.
+This attach method is used when the DND action is `private'.
+This is also used when `org-yank-image-save-method' is nil.
+When nil, use `org-attach-method'."
+  :group 'org
+  :package-version '(Org . "9.7")
+  :type '(choice (const :tag "Default attach method" nil)
+                 (const :tag "Copy" cp)
+                 (const :tag "Move" mv)
+                 (const :tag "Hard link" ln)
+                 (const :tag "Symbolic link" lns)))
+
+(declare-function mailcap-file-name-to-mime-type "mailcap" (file-name))
+(defvar org-attach-method)
+
+(defun org--dnd-rmc (prompt choices)
+  (if (null (use-dialog-box-p))
+      (caddr (read-multiple-choice prompt choices))
+    (setq choices
+          (mapcar
+           (pcase-lambda (`(_key ,message ,val))
+             (cons (capitalize message) val))
+           choices))
+    (x-popup-menu t (list prompt (cons "" choices)))))
+
+(defun org--dnd-local-file-handler (url action)
+  "Handle file URL as per ACTION."
+  (let ((method (if (eq org-yank-dnd-method 'ask)
+                    (org--dnd-rmc
+                     "What to do with file?"
+                     '((?a "attach" attach)
+                       (?o "open" open)
+                       (?f "insert file: link" file-link)))
+                  org-yank-dnd-method)))
+    (pcase method
+      (`attach (org--dnd-attach-file url action))
+      (`open (dnd-open-local-file url action))
+      (`file-link
+       (let ((filename (dnd-get-local-file-name url)))
+         (insert (org-link-make-string (concat "file:" filename))))))))
+
+(defun org--dnd-attach-file (url action)
+  "Attach filename given by URL using method pertaining to ACTION.
+If ACTION is `move', use `mv' attach method.
+If `copy', use `cp' attach method.
+If `ask', ask the user.
+If `private', use the method denoted in `org-yank-dnd-default-attach-method'.
+The action `private' is always returned."
+  (require 'mailcap)
+  (let* ((filename (dnd-get-local-file-name url))
+         (mimetype (mailcap-file-name-to-mime-type filename))
+         (separatep (and (string-prefix-p "image/" mimetype)
+                         (not (eq 'attach org-yank-image-save-method))))
+         (method (pcase action
+                   ('copy 'cp)
+                   ('move 'mv)
+                   ('ask (org--dnd-rmc
+                          "Attach using method"
+                          '((?c "copy" cp)
+                            (?m "move" mv)
+                            (?l "hard link" ln)
+                            (?s "symbolic link" lns))))
+                   ('private (or org-yank-dnd-default-attach-method
+                                 org-attach-method)))))
+    (if separatep
+        (funcall
+         (pcase method
+           ('cp #'copy-file)
+           ('mv #'rename-file)
+           ('ln #'add-name-to-file)
+           ('lns #'make-symbolic-link))
+         filename
+         (expand-file-name (file-name-nondirectory filename)
+                           org-yank-image-save-method))
+      (org-attach-attach filename nil method))
+    (insert
+     (org-link-make-string
+      (concat (if separatep
+                  "file:"
+                "attachment:")
+              (if separatep
+                  (expand-file-name (file-name-nondirectory filename)
+                                    org-yank-image-save-method)
+                (file-name-nondirectory filename))))
+     "\n")
+    'private))
+
+(defvar-local org--dnd-xds-method nil
+  "The method to use for dropped file.")
+(defun org--dnd-xds-function (need-name filename)
+  "Handle file with FILENAME dropped via XDS protocol.
+When NEED-NAME is t, FILNAME is the base name of the file to be
+saved.
+When NEED-NAME is nil, the drop is complete."
+  (if need-name
+      (let ((method (if (eq org-yank-dnd-method 'ask)
+                        (org--dnd-rmc
+                         "What to do with dropped file?"
+                         '((?a "attach" attach)
+                           (?o "open" open)
+                           (?f "insert file: link" file-link)))
+                      org-yank-dnd-method)))
+        (setq-local org--dnd-xds-method method)
+        (pcase method
+          (`attach (expand-file-name filename (org-attach-dir 'create)))
+          (`open (expand-file-name (make-temp-name "emacs.") temporary-file-directory))
+          (`file-link (read-file-name "Write file to: " nil nil nil filename))))
+    (pcase org--dnd-xds-method
+      (`attach (insert (org-link-make-string
+                        (concat "attachment:" (file-name-nondirectory filename)))
+                       "\n"))
+      (`file-link (insert (org-link-make-string (concat "file:" filename))
+                          "\n"))
+      (`open (find-file filename)))
+    (setq-local org--dnd-xds-method nil)))
+
 ;;; Other stuff
 
 (defvar reftex-docstruct-symbol)
-- 
2.42.0


[-- Attachment #3: Type: text/plain, Size: 2199 bytes --]


> I am attaching my further amendments on top of your patch.
> For simpler review, I am also adding the manual section verbatim.
>
> To other readers of this thread: Please help reviewing the manual
> section - it should be as clear as possible for everyone.

Thanks for your review.  The manual now says:

** Drag and Drop & ~yank-media~
:PROPERTIES:
:DESCRIPTION: Dropping and pasting files and images
:END:

#+cindex: dropping files
#+cindex: dragging files
#+cindex: drag and drop
#+cindex: dnd
#+vindex: org-yank-dnd-method
Org mode supports drag and drop (DnD) of files.  By default, Org asks
the user what must be done with the dropped file: attach it, insert
=file:= link, or open the file.  Customize ~org-yank-dnd-method~ to
set the default DnD action.

When DnD method is "attach", Org mode first consults DnD metadata to
decide the attach method.  For example, when file/files are dragged
from a file manager, Org may attach by copying or by moving.

#+vindex: org-yank-dnd-default-attach-method
If Org cannot figure out which attachment method to use from the
metadata, it defaults to ~org-yank-dnd-default-attach-method~ [fn::By
default, ~org-yank-dnd-default-attach-method~ is set to nil -- use the same
value as ~org-attach-method~ (~cp~ by default).]

#+cindex: pasting files, images from clipboard
Starting from Emacs 29, Org mode supports ~yank-media~ command to yank
images from the clipboard and files from a file manager.

#+vindex: org-yank-image-save-method
When yanking images from clipboard, Org saves the image on disk and
inserts the image link to Org buffer.  Images are either saved as
attachments to heading (default) or to a globally defined directory.
The save location is controlled by ~org-yank-image-save-method~.

#+vindex: org-yank-image-file-name-function
The yanked images are saved under automatically generated name.  You
can customize ~org-yank-image-file-name-function~ to make Org query
the image names or change the naming scheme.

When yanking files copied from a file manager, Org respects the value
of ~org-yank-dnd-method~.  Image files pasted this way also respect
the value of ~org-yank-image-save-method~ when the action to perform
is =attach=.

  reply	other threads:[~2023-10-23 10:13 UTC|newest]

Thread overview: 49+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <87bkdccihf.fsf.ref@yahoo.com>
2023-09-22 14:52 ` [BUG] [PATCH] Add yank-media and DND handler [9.6.7 (9.6.7-g6eb773 @ /home/viz/lib/emacs/straight/build/org/)] Visuwesh
2023-09-22 16:51   ` Max Nikulin
2023-09-22 17:29     ` Visuwesh
2023-09-24  8:06       ` Max Nikulin
2023-09-23 10:28   ` Ihor Radchenko
2023-09-23 16:55     ` Visuwesh
2023-09-25 13:14       ` Visuwesh
2023-09-26 16:25         ` Max Nikulin
2023-09-27  8:33           ` Visuwesh
2023-10-07 11:56           ` Ihor Radchenko
2023-10-07 12:07         ` Ihor Radchenko
2023-10-07 12:27           ` Visuwesh
2023-10-07 12:36             ` Ihor Radchenko
2023-10-07 14:03             ` Visuwesh
2023-10-08  9:30               ` Ihor Radchenko
2023-10-08 11:21                 ` Visuwesh
2023-10-09 11:12                   ` Ihor Radchenko
2023-10-09 12:17                     ` Visuwesh
2023-10-19  7:34                       ` Visuwesh
2023-10-19  9:44                         ` Ihor Radchenko
2023-10-20  1:52                           ` Po Lu
2023-10-20  7:29                             ` Ihor Radchenko
2023-10-20  7:46                               ` Po Lu
2023-10-20  7:57                                 ` Ihor Radchenko
2023-10-20  8:29                                   ` Po Lu
2023-10-20 10:17                                   ` Visuwesh
2023-10-22  6:19                     ` Visuwesh
2023-10-23  8:58                       ` Ihor Radchenko
2023-10-23 10:12                         ` Visuwesh [this message]
2023-10-26 11:39                           ` Po Lu
2023-11-05 12:02                             ` Ihor Radchenko
2023-11-05 17:45                               ` Visuwesh
2023-12-05 13:18                               ` Visuwesh
2023-12-10 13:53                                 ` Ihor Radchenko
2023-12-10 14:47                                   ` Bastien Guerry
2023-12-10 15:07                                     ` Ihor Radchenko
2023-09-24 14:58     ` Max Nikulin
2023-09-25 14:15       ` Visuwesh
2023-09-26 10:24         ` Ihor Radchenko
2023-09-27  8:29           ` Visuwesh
2023-09-28 12:01             ` Max Nikulin
2023-09-24 14:49   ` Max Nikulin
2023-10-06  7:34   ` Po Lu
2023-09-29  8:20 Liu Hui
2023-10-01 14:28 ` Visuwesh
2023-10-02  0:28   ` Liu Hui
  -- strict thread matches above, loose matches on Subject: below --
2023-10-11 14:24 Liu Hui
2023-10-11 15:36 ` Visuwesh
2023-10-12  5:12   ` Liu Hui

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.orgmode.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=87a5s98x67.fsf@gmail.com \
    --to=visuweshm@gmail.com \
    --cc=emacs-orgmode@gnu.org \
    --cc=luangruo@yahoo.com \
    --cc=yantar92@posteo.net \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).