improve images

* Let the user choose the directory.
* Rework of Files.Images.
* area.currentImage is now an ... image, not an index.
* Add 'switch-image-file-reverse' keybinding.
* Add image gicons (from thumbnails) in the submenu.
* Images are reset on drawing mode left or on images added from the clipboard.
This commit is contained in:
abakkk 2020-09-11 04:06:21 +02:00
parent a06d318646
commit 771bad2d59
10 changed files with 225 additions and 97 deletions

View File

@ -42,7 +42,7 @@ Then save your beautiful work by taking a screenshot.
* Insertable images:
Add your images (jpeg, png, svg) to `~/.local/share/drawOnYourScreen/images/`.
You can insert images (jpeg, png, svg) in your drawings. By default images are sought in `~/.local/share/drawOnYourScreen/images/` but the location is configurable in the preferences. Another way is to copy-past the images from Nautilus or any clipboard source by using the usual `Ctrl + V` shortcut inside the drawing mode.
* Eraser and SVG:

43
area.js
View File

@ -86,7 +86,7 @@ var DrawingArea = new Lang.Class({
this.undoneElements = [];
this.currentElement = null;
this.currentTool = Shapes.NONE;
this.currentImage = 0;
this.currentImage = null;
this.currentTextRightAligned = Clutter.get_default_text_direction() == Clutter.TextDirection.RTL;
let fontName = St.Settings && St.Settings.get().font_name || Convenience.getSettings('org.gnome.desktop.interface').get_string('font-name');
this.currentFont = Pango.FontDescription.from_string(fontName);
@ -150,6 +150,17 @@ var DrawingArea = new Lang.Class({
this.colors.push(Clutter.Color.get_static(Clutter.StaticColor.WHITE));
},
get currentImage() {
if (!this._currentImage)
this._currentImage = Files.Images.getNext(this._currentImage);
return this._currentImage;
},
set currentImage(image) {
this._currentImage = image;
},
get currentFontFamily() {
return this.currentFont.get_family();
},
@ -188,13 +199,6 @@ var DrawingArea = new Lang.Class({
this.currentFillRule = evenodd ? Cairo.FillRule.EVEN_ODD : Cairo.FillRule.WINDING;
},
getImages() {
let images = Files.Images.getImages();
if (!images[this.currentImage])
this.currentImage = Math.max(images.length - 1, 0);
return images;
},
get fontFamilies() {
if (!this._fontFamilies) {
let otherFontFamilies = Elements.getAllFontFamilies().filter(family => {
@ -601,14 +605,11 @@ var DrawingArea = new Lang.Class({
points: []
});
} else if (this.currentTool == Shapes.IMAGE) {
let images = this.getImages();
if (!images.length)
return;
this.currentElement = new Elements.DrawingElement({
shape: this.currentTool,
color: this.currentColor.to_string(),
eraser: eraser,
image: images[this.currentImage],
image: this.currentImage,
operator: this.currentOperator,
points: []
});
@ -988,21 +989,18 @@ var DrawingArea = new Lang.Class({
this.emit('show-osd', null, DisplayStrings.getTextAlignment(this.currentTextRightAligned), "", -1, false);
},
switchImageFile: function() {
let images = this.getImages();
if (!images.length)
return;
if (images.length > 1)
this.currentImage = this.currentImage == images.length - 1 ? 0 : this.currentImage + 1;
this.emit('show-osd', images[this.currentImage].gicon, images[this.currentImage].toString(), "", -1, false);
switchImageFile: function(reverse) {
this.currentImage = Files.Images[reverse ? 'getPrevious' : 'getNext'](this.currentImage);
if (this.currentImage)
this.emit('show-osd', this.currentImage.gicon, this.currentImage.toString(), "", -1, false);
},
pasteImageFiles: function() {
Files.Images.addImagesFromClipboard((images, index) => {
this.currentImage = index;
Files.Images.addImagesFromClipboard(lastImage => {
this.currentImage = lastImage;
this.currentTool = Shapes.IMAGE;
this.updatePointerCursor();
this.emit('show-osd', images[this.currentImage].gicon, images[this.currentImage].toString(), "", -1, false);
this.emit('show-osd', this.currentImage.gicon, this.currentImage.toString(), "", -1, false);
});
},
@ -1088,6 +1086,7 @@ var DrawingArea = new Lang.Class({
this._redisplay();
this.closeMenu();
this.get_parent().set_background_color(null);
Files.Images.reset();
if (save)
this.savePersistent();
},

View File

@ -183,7 +183,8 @@ const AreaManager = new Lang.Class({
'switch-fill-rule': this.activeArea.switchFillRule.bind(this.activeArea),
'switch-dash' : this.activeArea.switchDash.bind(this.activeArea),
'switch-fill' : this.activeArea.switchFill.bind(this.activeArea),
'switch-image-file' : this.activeArea.switchImageFile.bind(this.activeArea),
'switch-image-file' : this.activeArea.switchImageFile.bind(this.activeArea, false),
'switch-image-file-reverse' : this.activeArea.switchImageFile.bind(this.activeArea, true),
'select-none-shape': () => this.activeArea.selectTool(Area.Tools.NONE),
'select-line-shape': () => this.activeArea.selectTool(Area.Tools.LINE),
'select-ellipse-shape': () => this.activeArea.selectTool(Area.Tools.ELLIPSE),

117
files.js
View File

@ -31,8 +31,8 @@ const St = imports.gi.St;
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const EXAMPLE_IMAGES = Me.dir.get_child('data').get_child('images');
const USER_IMAGES = Gio.File.new_for_path(GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images']));
const EXAMPLE_IMAGE_DIRECTORY = Me.dir.get_child('data').get_child('images');
const DEFAULT_USER_IMAGE_LOCATION = GLib.build_filenamev([GLib.get_user_data_dir(), Me.metadata['data-dir'], 'images']);
const Clipboard = St.Clipboard.get_default();
const CLIPBOARD_TYPE = St.ClipboardType.CLIPBOARD;
const ICON_DIR = Me.dir.get_child('data').get_child('icons');
@ -66,6 +66,11 @@ var Image = new Lang.Class({
_init: function(params) {
for (let key in params)
this[key] = params[key];
if (this.info) {
this.displayName = this.info.get_display_name();
this.contentType = this.info.get_content_type();
}
},
toString: function() {
@ -81,13 +86,31 @@ var Image = new Lang.Class({
};
},
// only called from menu so file exists
get thumbnailFile() {
if (!this._thumbnailFile) {
if (this.info.has_attribute('thumbnail::path') && this.info.get_attribute_boolean('thumbnail::is-valid')) {
let thumbnailPath = this.info.get_attribute_as_string('thumbnail::path');
this._thumbnailFile = Gio.File.new_for_path(thumbnailPath);
}
}
return this._thumbnailFile || null;
},
// only called from menu or area so this.file exists
get gicon() {
if (!this._gicon)
this._gicon = new Gio.FileIcon({ file: this.file });
this._gicon = new Gio.FileIcon({ file: this.thumbnailFile || this.file });
return this._gicon;
},
// use only thumbnails in menu (memory)
get thumbnailGicon() {
if (this.contentType != 'image/svg+xml' && !this.thumbnailFile)
return null;
return this.gicon;
},
get bytes() {
if (!this._bytes) {
if (this.file)
@ -95,7 +118,7 @@ var Image = new Lang.Class({
// load_bytes available in GLib 2.56+
this._bytes = this.file.load_bytes(null)[0];
} catch(e) {
let [success_, contents] = this.file.load_contents(null);
let [, contents] = this.file.load_contents(null);
if (contents instanceof Uint8Array)
this._bytes = ByteArray.toGBytes(contents);
else
@ -151,34 +174,73 @@ var Image = new Lang.Class({
}
});
// Get images with getPrevious, getNext, or by iterating over it.
var Images = {
clipboardImages: [],
_images: [],
_clipboardImages: [],
_upToDate: false,
getImages: function() {
let images = [];
_clipboardImagesContains: function(file) {
return this._clipboardImages.some(image => image.file.equal(file));
},
_getImages: function() {
if (this._upToDate)
return this._images;
[EXAMPLE_IMAGES, USER_IMAGES].forEach(directory => {
let images = [];
let userLocation = Me.drawingSettings.get_string('image-location') || DEFAULT_USER_IMAGE_LOCATION;
let userDirectory = Gio.File.new_for_commandline_arg(userLocation);
[EXAMPLE_IMAGE_DIRECTORY, userDirectory].forEach(directory => {
let enumerator;
try {
enumerator = directory.enumerate_children('standard::display-name,standard::content-type', Gio.FileQueryInfoFlags.NONE, null);
enumerator = directory.enumerate_children('standard::,thumbnail::', Gio.FileQueryInfoFlags.NONE, null);
} catch(e) {
return;
}
let fileInfo = enumerator.next_file(null);
while (fileInfo) {
if (fileInfo.get_content_type().indexOf('image') == 0)
images.push(new Image({ file: enumerator.get_child(fileInfo), contentType: fileInfo.get_content_type(), displayName: fileInfo.get_display_name() }));
fileInfo = enumerator.next_file(null);
let info = enumerator.next_file(null);
while (info) {
let file = enumerator.get_child(info);
if (info.get_content_type().indexOf('image') == 0 && !this._clipboardImagesContains(file)) {
let index = this._images.findIndex(image => image.file.equal(file));
if (index != -1)
images.push(this._images[index]);
else
images.push(new Image({ file, info }));
}
info = enumerator.next_file(null);
}
enumerator.close(null);
});
images.sort((a, b) => {
return a.displayName.localeCompare(b.displayName);
});
return images.concat(this.clipboardImages);
this._images = images.concat(this._clipboardImages)
.sort((a, b) => a.toString().localeCompare(b.toString()));
this._upToDate = true;
return this._images;
},
[Symbol.iterator]: function() {
return this._getImages()[Symbol.iterator]();
},
getNext: function(currentImage) {
let images = this._getImages();
let index = currentImage ? images.findIndex(image => image.file.equal(currentImage.file)) : 0;
return images[index == images.length - 1 ? 0 : index + 1] || null;
},
getPrevious: function(currentImage) {
let images = this._getImages();
let index = currentImage ? images.findIndex(image => image.file.equal(currentImage.file)) : 0;
return images[index <= 0 ? images.length - 1 : index - 1] || null;
},
reset: function() {
this._upToDate = false;
},
addImagesFromClipboard: function(callback) {
@ -193,20 +255,21 @@ var Images = {
let images = lines.filter(line => !!line)
.map(line => Gio.File.new_for_commandline_arg(line))
.filter(file => file.query_exists(null))
.map(file => [file, file.query_info('standard::display-name,standard::content-type', Gio.FileQueryInfoFlags.NONE, null)])
.map(file => [file, file.query_info('standard::,thumbnail::', Gio.FileQueryInfoFlags.NONE, null)])
.filter(pair => pair[1].get_content_type().indexOf('image') == 0)
.map(pair => new Image({ file: pair[0], contentType: pair[1].get_content_type(), displayName: pair[1].get_display_name() }));
.map(pair => new Image({ file: pair[0], info: pair[1] }));
// Prevent duplicated
images.filter(image => !this.clipboardImages.map(clipboardImage => clipboardImage.file).some(clipboardFile => clipboardFile.equal(image.file)))
.forEach(image => this.clipboardImages.push(image));
images.filter(image => !this._clipboardImagesContains(image.file))
.forEach(image => this._clipboardImages.push(image));
if (images.length) {
this.reset();
let allImages = this._getImages();
let lastFile = images[images.length - 1].file;
let allImages = this.getImages();
let index = allImages.findIndex(image => image.file.equal(lastFile));
callback(allImages, index);
}
callback(allImages[index]);
}
});
}
};

View File

@ -10,7 +10,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Draw On Your Screen\n"
"Report-Msgid-Bugs-To: https://framagit.org/abakkk/DrawOnYourScreen/issues\n"
"POT-Creation-Date: 2020-09-09 08:40+0200\n"
"POT-Creation-Date: 2020-09-11 03:43+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -502,6 +502,12 @@ msgstr ""
msgid "The line width in pixels"
msgstr ""
msgid "Image location"
msgstr ""
msgid "The location of the directory in which the image tool picks"
msgstr ""
msgid "Color palettes"
msgstr ""
@ -544,6 +550,9 @@ msgstr ""
msgid "Open previous drawing"
msgstr ""
msgid "Paste images from the clipboard"
msgstr ""
msgid "Redo last brushstroke"
msgstr ""
@ -640,7 +649,10 @@ msgstr ""
msgid "Change font weight"
msgstr ""
msgid "Change image file"
msgid "Change image"
msgstr ""
msgid "Change image (reverse)"
msgstr ""
msgid "Change linecap"

80
menu.js
View File

@ -262,12 +262,7 @@ var DrawingMenu = new Lang.Class({
this.fontSection = fontSection;
let imageSection = new PopupMenu.PopupMenuSection();
let images = this.area.getImages();
if (images.length) {
if (this.area.currentImage > images.length - 1)
this.area.currentImage = images.length - 1;
this._addSubMenuItem(imageSection, null, images, this.area, 'currentImage');
}
this._addImageSubMenuItem(imageSection);
this._addSeparator(imageSection);
this.menu.addMenuItem(imageSection);
imageSection.itemActivated = () => {};
@ -398,34 +393,27 @@ var DrawingMenu = new Lang.Class({
},
_addSubMenuItem: function(menu, icon, obj, target, targetProperty, callback) {
if (targetProperty == 'currentImage')
icon = obj[target[targetProperty]].gicon;
let item = new PopupMenu.PopupSubMenuMenuItem(String(obj[target[targetProperty]]), icon ? true : false);
if (icon && icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon))
item.icon.set_gicon(icon);
else if (icon)
item.icon.set_icon_name(icon);
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.itemActivated = item.menu.close;
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
for (let i in obj) {
Object.keys(obj).forEach(key => {
let text;
if (targetProperty == 'currentFontWeight')
text = `<span font_weight="${i}">${obj[i]}</span>`;
text = `<span font_weight="${key}">${obj[key]}</span>`;
else if (targetProperty == 'currentFontStyle')
text = `<span font_style="${DisplayStrings.FontStyleMarkup[i]}">${obj[i]}</span>`;
text = `<span font_style="${DisplayStrings.FontStyleMarkup[key]}">${obj[key]}</span>`;
else
text = String(obj[i]);
text = String(obj[key]);
let iCaptured = Number(i);
let subItem = item.menu.addAction(text, () => {
item.label.set_text(String(obj[iCaptured]));
target[targetProperty] = iCaptured;
if (targetProperty == 'currentImage')
item.icon.set_gicon(obj[iCaptured].gicon);
item.label.set_text(String(obj[key]));
target[targetProperty] = Number(key);
if (callback)
callback();
});
@ -434,13 +422,14 @@ var DrawingMenu = new Lang.Class({
getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment);
// change the display order of tools
if (obj == DisplayStrings.Tool && i == this.drawingTools.POLYGON)
if (obj == DisplayStrings.Tool && Number(key) == this.drawingTools.POLYGON)
item.menu.moveMenuItem(subItem, 4);
else if (obj == DisplayStrings.Tool && i == this.drawingTools.POLYLINE)
else if (obj == DisplayStrings.Tool && Number(key) == this.drawingTools.POLYLINE)
item.menu.moveMenuItem(subItem, 5);
}
});
return GLib.SOURCE_REMOVE;
});
menu.addMenuItem(item);
},
@ -449,9 +438,7 @@ var DrawingMenu = new Lang.Class({
let item = new PopupMenu.PopupSubMenuMenuItem(text, true);
item.icon.set_gicon(Files.Icons.PALETTE);
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.itemActivated = item.menu.close;
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
this.area.palettes.forEach(palette => {
@ -468,6 +455,7 @@ var DrawingMenu = new Lang.Class({
});
return GLib.SOURCE_REMOVE;
});
menu.addMenuItem(item);
return item;
},
@ -478,9 +466,7 @@ var DrawingMenu = new Lang.Class({
item.icon.set_gicon(Files.Icons.COLOR);
item.icon.set_style(`color:${this.area.currentColor.to_string().slice(0, 7)};`);
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.itemActivated = item.menu.close;
this._populateColorSubMenu();
menu.addMenuItem(item);
@ -508,9 +494,7 @@ var DrawingMenu = new Lang.Class({
let item = new PopupMenu.PopupSubMenuMenuItem(DisplayStrings.getFontFamily(this.area.currentFontFamily), true);
item.icon.set_icon_name(icon);
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.itemActivated = item.menu.close;
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {
@ -531,6 +515,30 @@ var DrawingMenu = new Lang.Class({
menu.addMenuItem(item);
},
_addImageSubMenuItem: function(menu, images) {
let item = new PopupMenu.PopupSubMenuMenuItem(this.area.currentImage.toString(), true);
item.icon.set_gicon(this.area.currentImage.gicon);
item.menu.itemActivated = item.menu.close;
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {
if (!item.menu.isOpen && item.menu.isEmpty()) {
[...Files.Images].forEach(image => {
let subItem = item.menu.addAction(image.toString(), () => {
item.label.set_text(image.toString());
this.area.currentImage = image;
item.icon.set_gicon(image.gicon);
}, image.thumbnailGicon || undefined);
getActor(subItem).connect('key-focus-in', updateSubMenuAdjustment);
});
}
item.menu.openOld();
};
menu.addMenuItem(item);
},
_addDrawingNameItem: function(menu) {
this.drawingNameMenuItem = new PopupMenu.PopupMenuItem('', { reactive: false, activate: false });
this.drawingNameMenuItem.setSensitive(false);
@ -554,9 +562,7 @@ var DrawingMenu = new Lang.Class({
item.setSensitive(Boolean(Files.getJsons().length));
item.icon.set_icon_name('document-open-symbolic');
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.itemActivated = item.menu.close;
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {
@ -609,9 +615,7 @@ var DrawingMenu = new Lang.Class({
this.saveDrawingSubMenu = item.menu;
item.icon.set_icon_name('document-save-symbolic');
item.menu.itemActivated = () => {
item.menu.close();
};
item.menu.itemActivated = item.menu.close;
item.menu.openOld = item.menu.open;
item.menu.open = (animate) => {

View File

@ -23,6 +23,7 @@
const Atk = imports.gi.Atk;
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
@ -268,6 +269,14 @@ const DrawingPage = new GObject.Class({
dashOffsetRow.addWidget(dashOffsetButton);
toolsListBox.add(dashOffsetRow);
let imageLocationRow = new PrefRow({ label: this.schema.get_key('image-location').get_summary() });
let imageLocationButton = new FileChooserButton({ action: Gtk.FileChooserAction.SELECT_FOLDER,
name: this.schema.get_key('image-location').get_summary(),
tooltip_text: this.schema.get_key('image-location').get_description() });
this.settings.bind('image-location', imageLocationButton, 'location', 0);
imageLocationRow.addWidget(imageLocationButton);
toolsListBox.add(imageLocationRow);
let resetButton = new Gtk.Button({ label: _("Reset settings"), halign: Gtk.Align.CENTER });
resetButton.get_style_context().add_class('destructive-action');
resetButton.connect('clicked', () => this.schema.list_keys().forEach(key => this.settings.reset(key)));
@ -565,8 +574,7 @@ const ColorStringButton = new GObject.Class({
Extends: Gtk.ColorButton,
Properties: {
'color-string': GObject.ParamSpec.string('color-string', 'colorString', 'A string that describes the color',
GObject.ParamFlags.READWRITE,
'black')
GObject.ParamFlags.READWRITE, 'black')
},
get color_string() {
@ -593,6 +601,37 @@ const ColorStringButton = new GObject.Class({
}
});
const FileChooserButton = new GObject.Class({
Name: 'DrawOnYourScreenFileChooserButton',
GTypeName: 'DrawOnYourScreenFileChooserButton',
Extends: Gtk.FileChooserButton,
Properties: {
'location': GObject.ParamSpec.string('location', 'location', 'location',
GObject.ParamFlags.READWRITE, '')
},
get location() {
return this.get_file().get_path();
},
set location(location) {
if (!location) {
this.unselect_all();
if (this.get_file())
this.set_file(Gio.File.new_for_path('aFileThatDoesNotExist'));
return;
}
let file = Gio.File.new_for_commandline_arg(location);
if (file.query_exists(null))
this.set_file(file);
},
vfunc_file_set: function(args) {
this.notify('location');
}
});
// this code comes from Sticky Notes View by Sam Bull, https://extensions.gnome.org/extension/568/notes/
const KeybindingsWidget = new GObject.Class({
Name: 'DrawOnYourScreenKeybindings.Widget',

Binary file not shown.

View File

@ -87,6 +87,11 @@
<summary>Grid overlay line width</summary>
<description>The line width in pixels</description>
</key>
<key type="s" name="image-location">
<default>""</default>
<summary>Image location</summary>
<description>The location of the directory in which the image tool picks</description>
</key>
<key type="a(sas)" name="palettes">
<default>
[
@ -148,7 +153,7 @@
</key>
<key type="as" name="paste-image-files">
<default>["&lt;Primary&gt;v"]</default>
<summary>Paste image files from the clipboard</summary>
<summary>Paste images from the clipboard</summary>
</key>
<key type="as" name="redo">
<default>["&lt;Primary&gt;&lt;Shift&gt;z"]</default>
@ -284,8 +289,12 @@
<summary>Change font weight</summary>
</key>
<key type="as" name="switch-image-file">
<default>["&lt;Primary&gt;&lt;Shift&gt;i"]</default>
<summary>Change image file</summary>
<default>["&lt;Primary&gt;&lt;Alt&gt;i"]</default>
<summary>Change image</summary>
</key>
<key type="as" name="switch-image-file-reverse">
<default>["&lt;Primary&gt;&lt;Alt&gt;&lt;Shift&gt;i"]</default>
<summary>Change image (reverse)</summary>
</key>
<key type="as" name="switch-linecap">
<default>["&lt;Primary&gt;k"]</default>

View File

@ -49,7 +49,8 @@ var INTERNAL_KEYBINDINGS = [
['switch-fill', 'switch-fill-rule', 'switch-color-palette', 'switch-color-palette-reverse'],
['increment-line-width', 'increment-line-width-more', 'decrement-line-width', 'decrement-line-width-more',
'switch-linejoin', 'switch-linecap', 'switch-dash'],
['switch-font-family', 'switch-font-family-reverse', 'switch-font-weight', 'switch-font-style', 'switch-text-alignment', 'switch-image-file'],
['switch-font-family', 'switch-font-family-reverse', 'switch-font-weight', 'switch-font-style', 'switch-text-alignment'],
['switch-image-file', 'switch-image-file-reverse', 'paste-image-files'],
['toggle-panel-and-dock-visibility', 'toggle-background', 'toggle-grid', 'toggle-square-area'],
['open-next-json', 'open-previous-json', 'save-as-json', 'save-as-svg', 'open-preferences', 'toggle-help'],
];