Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions mint-screenshot@khumnath/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Mint Screenshot

A modern screenshot and annotation applet for Cinnamon. Capture your screen, annotate with shapes, arrows, and text, then save or copy to clipboard — all from a single streamlined workflow.

## Why Mint Screenshot?

The default Cinnamon screenshot tool (`gnome-screenshot`) captures the screen but offers **no built-in annotation**. You have to open a separate image editor just to draw an arrow or highlight something. Mint Screenshot solves this by combining capture and annotation into one tool:

| Feature | Default Screenshot Tool | Mint Screenshot |
|---------|------------------------|-----------------|
| Fullscreen capture | ✅ | ✅ |
| Area selection | ✅ | ✅ (live preview with dimmed overlay) |
| Timed capture | ✅ (fixed delays) | ✅ (1–999s, custom countdown with floating pill) |
| Window capture | ✅ | ✅ (X11) |
**Wayland support** | ✅ | ✅ Via XDG Desktop Portal |
| **Draw annotations** | ❌ | ✅ Rectangles, ellipses, arrows, freehand, highlights |
| **Add text** | ❌ | ✅ Resizable text with Pango rendering |
| **Crop after capture** | ❌ | ✅ Drag handles to adjust region |
| **Undo/Redo** | ❌ | ✅ Full history with Ctrl+Z/Y |
| **Color palette** | ❌ | ✅ 6 colors, adjustable line width |
| **Move/resize/rotate** | ❌ | ✅ Per-annotation context toolbar |
| **Delete annotations** | ❌ | ✅ Delete key or toolbar button |
| **HiDPI support** | Partial | ✅ Pixel-perfect on scaled displays |
| **Save format options** | PNG only | ✅ PNG, JPG, GIF with quality presets |
| **Panel integration** | No panel applet | ✅ Native Cinnamon panel applet |

## Features

* **Capture modes**: Fullscreen, area selection, and timed capture with custom countdown
* **Annotation tools**: Rectangle, ellipse, arrow, freehand draw, highlight, and text — all with adjustable color and line width
* **Non-destructive editing**: Move, resize, rotate, and delete any annotation after placing it
* **Export options**: Save to disk (PNG/JPG/GIF), copy to clipboard, or use Save As dialog
* **Quality presets**: Original, Medium, and Space Saver compression levels
* **Floating toolbar**: Adaptive Material Design toolbar that snaps between top and bottom of screen
* **Keyboard driven**: Full keyboard shortcut support (Ctrl+S, Ctrl+C, Ctrl+Z, Ctrl+Y, Delete, Escape)
* **Cross-session**: Works on both X11 and Wayland (via D-Bus XDG Desktop Portal)
* **Localization ready**: Gettext-based i18n with included translations

## Requirements

* **Cinnamon 4.0+** (Cinnamon 6.0+ recommended)
* Python 3
* GTK 3 (`python3-gi`, `python3-gi-cairo`, `python3-cairo`)
* Pillow (`python3-pil`) for icon processing
* X11: `gir1.2-wnck-3.0` for window detection
* Wayland: `python3-dbus` for portal support

On Linux Mint / Ubuntu, install dependencies with:
```
sudo apt install python3-gi python3-gi-cairo python3-cairo python3-pil gir1.2-wnck-3.0
```

> **Note**: The applet will check for missing dependencies on first launch and show you exactly which packages to install.

## Installation

1. Right-click on the Cinnamon panel and click **Applets**
2. Go to the **Download** tab and search for **Mint Screenshot**
3. Click **Install**
4. Switch to the **Manage** tab and add **Mint Screenshot** to your panel

## Keyboard Shortcuts

| Shortcut | Action |
|----------|--------|
| `Ctrl+S` | Save screenshot (opens Save As dialog) |
| `Ctrl+C` | Copy to clipboard |
| `Ctrl+Z` | Undo last annotation |
| `Ctrl+Y` or `Ctrl+Shift+Z` | Redo |
| `Delete` | Remove selected annotation |
| `Escape` | Exit the tool |

## Feedback

You can leave a comment on [cinnamon-spices.linuxmint.com](https://cinnamon-spices.linuxmint.com) or create an issue on the development repository:

https://github.com/khumnath/mint-screenshot

If you find this applet useful, please consider leaving a rating — it helps others discover it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Annotation:
"""Holds position, style, and type data for a single drawn shape or text label."""

__slots__ = ('type', 'start', 'end', 'color', 'width', 'text', 'angle',
'points', 'sx', 'sy', '_cached_layout', '_layout_cache_key')

def __init__(self, kind, start, end, color=(1, 0, 0), width=2, text=""):
self.type = kind # 'rect', 'arrow', 'text', 'ellipse', 'draw', 'highlight'
self.start = start # (x, y) origin
self.end = end # (x, y) opposite corner / endpoint
self.color = color
self.width = width
self.text = text
self.angle = 0 # rotation in radians
self.points = [] # freehand path points for 'draw' type
self.sx = 1.0 # user-applied horizontal scale
self.sy = 1.0 # user-applied vertical scale
self._cached_layout = None # cached Pango layout for text annotations
self._layout_cache_key = None # (text, width) key to invalidate cache
195 changes: 195 additions & 0 deletions mint-screenshot@khumnath/files/mint-screenshot@khumnath/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import os
import json
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, Pango
try:
import dbus
from dbus.mainloop.glib import DBusGMainLoop
except ImportError:
pass

from config import IS_WAYLAND, HAS_DBUS, APP_ICON_PIXBUF, _
from utils import load_settings
from launcher import LauncherMixin
from capture import CaptureMixin
from editor import EditorMixin
from canvas import CanvasMixin

class ScreenshotOverlay(Gtk.Window, LauncherMixin, CaptureMixin, EditorMixin, CanvasMixin):
def _on_size_changed(self, scale):
"""Slider callback: update stroke width."""
self.line_width = int(scale.get_value())
if self.selected_ann:
self.selected_ann.width = self.line_width
if self.selected_ann.type == 'text':
self._update_text_bounds(self.selected_ann)
self.queue_draw()

def _update_text_bounds(self, ann):
"""Recalculate the end coordinate for a text annotation based on current font size,
while preserving any manual scaling applied by the user."""
if ann.type != 'text': return
layout = self.create_pango_layout(ann.text)
# Base font size calculation
fs = 10 + (ann.width * 3)
layout.set_font_description(Pango.FontDescription(f"Sans Bold {fs}"))
tw, th = layout.get_pixel_size()

# Apply the stored scaling factors to the new natural size
ann.end = (ann.start[0] + (tw * ann.sx), ann.start[1] + (th * ann.sy))

def _on_reset_clicked(self, widget):
"""Clear everything and go back to the launcher."""
dialog = Gtk.MessageDialog(
transient_for=self,
flags=Gtk.DialogFlags.MODAL,
message_type=Gtk.MessageType.WARNING,
buttons=Gtk.ButtonsType.OK_CANCEL,
text=_("Reset Everything?")
)
dialog.format_secondary_text(_("This will clear all annotations and return to the launcher."))
response = dialog.run()
dialog.destroy()

if response == Gtk.ResponseType.OK:
self.rect = None
self.annotations = []
self.redo_stack = []
self.selection_start = None
self.selection_end = None
self.current_ann = None
self.selected_ann = None
self.hovered_ann = None
self.edit_mode = None
self.hovered_crop_handle = None
self.hovered_crop_outside = False
self._toolbar_at_bottom = False
self.state = None
self.full_pixbuf = None
self._invalidate_bg_cache()
self.hide()
if self.toolbar_box:
self.toolbar_box.hide()
# Remove entirely so it rebuilds fresh on re-entry
self.overlay.remove(self.toolbar_box)
self.toolbar_box = None
self._show_launcher()

def _set_tool_if_active(self, btn, tool):
"""Radio-button callback: switch the active drawing tool."""
if btn.get_active():
self.current_tool = tool
self.selected_ann = None
# Reset to base size when picking a new tool
self.line_width = self.base_line_width
if hasattr(self, 'size_scale'):
self.size_scale.set_value(self.line_width)
self.queue_draw()

def _set_color_if_active(self, btn, color):
"""Radio-button callback: switch drawing color."""
if btn.get_active():
self.current_color = color
if self.selected_ann:
self.selected_ann.color = color
self.queue_draw()

# --- Init ---

def __init__(self, save_dir_override=None):
super().__init__(type=Gtk.WindowType.TOPLEVEL)
self.set_role("mint-screenshot-tool")
if APP_ICON_PIXBUF:
self.set_icon(APP_ICON_PIXBUF)
else:
self.set_icon_name("applets-screenshooter-symbolic")
self.state = None
self.timer_expanded = False
self.is_wayland = IS_WAYLAND


self.selection_start = None
self.selection_end = None
self.rect = None
self.annotations = []
self.redo_stack = []
self.current_ann = None
self.selected_ann = None
self.hovered_ann = None
self.edit_mode = None
self.current_tool = 'select'
self.current_color = (0.2, 0.6, 1.0)
self.line_width = 3
self.mouse_pos = (0, 0)
self.hovered_window = None
self.is_full_capture = False

# User prefs (format, quality, save path)
self.settings = load_settings({
'format': 'png',
'quality': 'original',
'save_path': os.path.expanduser("~/Pictures"),
'show_labels': False,
'delay': 0,
'mode': 'area'
})

self.base_line_width = 3

# Override save path if the applet passed a directory via argv
if save_dir_override and os.path.isdir(save_dir_override):
self.settings['save_path'] = save_dir_override


self.metadata = self._load_project_metadata()

# Desktop size (multi-monitor total)
screen = Gdk.Screen.get_default()
self.width = screen.get_width()
self.height = screen.get_height()

# HiDPI scale factor (initial; updated after realize for accuracy)
self.scale = self.get_scale_factor()
self.connect("realize", lambda w: setattr(self, 'scale', self.get_scale_factor()))

self._init_dbus()

def _load_project_metadata(self):
try:

dir_path = os.path.dirname(os.path.realpath(__file__))
meta_path = os.path.join(dir_path, "metadata.json")
if os.path.exists(meta_path):
with open(meta_path, 'r') as f:
return json.load(f)
except Exception:
pass
return {
"version": "1.2.0",
"name": "Mint Screenshot",
"author": "Khumnath CG",
"website": "https://github.com/khumnath/mint-screenshot"
}

def _init_dbus(self):
if self.is_wayland and HAS_DBUS:
DBusGMainLoop(set_as_default=True)

self.full_pixbuf = None
self._show_launcher()



# --- Error dialogs ---

def _show_wayland_error(self, message):
"""Show a Wayland-specific error dialog."""
dialog = Gtk.MessageDialog(
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=_("Mint Screenshot — Wayland Error")
)
dialog.format_secondary_text(message)
dialog.run()
dialog.destroy()
46 changes: 46 additions & 0 deletions mint-screenshot@khumnath/files/mint-screenshot@khumnath/applet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const Applet = imports.ui.applet;
const Main = imports.ui.main;
const Util = imports.misc.util;
const GLib = imports.gi.GLib;
const Gettext = imports.gettext;

function _(str) {
return Gettext.dgettext("mint-screenshot", str);
}

const Settings = imports.ui.settings;

class MintScreenshotApplet extends Applet.IconApplet {
constructor(metadata, orientation, panel_height, instance_id) {
super(orientation, panel_height, instance_id);
this.metadata = metadata;
this.set_applet_icon_symbolic_name("applets-screenshooter-symbolic");
this.set_applet_tooltip(_("Mint Screenshot — Click to capture"));

this.settings = new Settings.AppletSettings(this, metadata.uuid, instance_id);
this.settings.bind("default-save-directory", "save_directory");
}

_takeScreenshot() {
let pythonScript = GLib.build_filenamev([this.metadata.path, "main.py"]);

// Resolve the save directory, respecting the user's locale for ~/Pictures
let picturesDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES) || GLib.get_home_dir();
let defaultSaveDir = GLib.build_filenamev([picturesDir, "Screenshots"]);

let saveDir = this.save_directory || defaultSaveDir;
if (saveDir.startsWith("~")) {
saveDir = saveDir.replace("~", GLib.get_home_dir());
}

Util.spawn(["python3", pythonScript, saveDir]);
}

on_applet_clicked(event) {
this._takeScreenshot();
}
}

function main(metadata, orientation, panel_height, instance_id) {
return new MintScreenshotApplet(metadata, orientation, panel_height, instance_id);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading